{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature pad",
  "description": "Draw a signature with mouse, finger or pen, or type it, and get a sharp PNG back.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/signature-pad/signature-pad.tsx",
      "content": "// Inspired by Watermelon UI's “Draw signature” (MIT, © 2026 Watermelon Platform Contributors,\n// github.com/WatermelonCorp/watermelon-platform). Rewritten with pointer events, a sharp canvas on\n// high-density screens, ink in the current text colour, and a typed fallback for keyboard users.\nimport { Check, Eraser, Keyboard, PenLine } from 'lucide-react'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useEffect, useRef, useState, type PointerEvent } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type Signature = { kind: 'drawn'; png: string } | { kind: 'typed'; name: string }\n\nexport type SignaturePadProps = {\n  onSign: (signature: Signature) => void\n  labels?: Partial<Record<'title' | 'clear' | 'done' | 'type' | 'draw' | 'typed' | 'signed' | 'pad', string>>\n  className?: string\n}\n\nconst EN = {\n  title: 'Sign here',\n  clear: 'Clear',\n  done: 'Done',\n  type: 'Type instead',\n  draw: 'Draw instead',\n  typed: 'Your full name',\n  signed: 'Signed',\n  pad: 'Signature area. Draw with a mouse, finger or pen.',\n}\n\nexport function SignaturePad({ onSign, labels = {}, className }: SignaturePadProps) {\n  const t = { ...EN, ...labels }\n  const canvas = useRef<HTMLCanvasElement>(null)\n  const last = useRef<{ x: number; y: number } | null>(null)\n  const [mode, setMode] = useState<'draw' | 'type'>('draw')\n  const [empty, setEmpty] = useState(true)\n  const [name, setName] = useState('')\n  const [signed, setSigned] = useState<Signature | null>(null)\n\n  // Size the bitmap to the element times the pixel ratio, so the ink stays sharp.\n  useEffect(() => {\n    const c = canvas.current\n    if (!c || mode !== 'draw' || signed) return\n    const fit = () => {\n      const r = c.getBoundingClientRect()\n      const dpr = window.devicePixelRatio || 1\n      c.width = Math.round(r.width * dpr)\n      c.height = Math.round(r.height * dpr)\n      const ctx = c.getContext('2d')!\n      ctx.scale(dpr, dpr)\n      ctx.lineCap = 'round'\n      ctx.lineJoin = 'round'\n      ctx.lineWidth = 2.5\n      ctx.strokeStyle = getComputedStyle(c).color\n      setEmpty(true)\n    }\n    fit()\n    const ro = new ResizeObserver(fit)\n    ro.observe(c)\n    return () => ro.disconnect()\n  }, [mode, signed])\n\n  const point = (e: PointerEvent<HTMLCanvasElement>) => {\n    const r = e.currentTarget.getBoundingClientRect()\n    return { x: e.clientX - r.left, y: e.clientY - r.top }\n  }\n\n  const down = (e: PointerEvent<HTMLCanvasElement>) => {\n    e.currentTarget.setPointerCapture(e.pointerId)\n    last.current = point(e)\n    const ctx = e.currentTarget.getContext('2d')!\n    ctx.beginPath()\n    ctx.arc(last.current.x, last.current.y, 1.2, 0, Math.PI * 2)\n    ctx.fillStyle = ctx.strokeStyle\n    ctx.fill()\n    setEmpty(false)\n  }\n\n  const move = (e: PointerEvent<HTMLCanvasElement>) => {\n    if (!last.current) return\n    const ctx = e.currentTarget.getContext('2d')!\n    const p = point(e)\n    ctx.beginPath()\n    ctx.moveTo(last.current.x, last.current.y)\n    ctx.lineTo(p.x, p.y)\n    ctx.stroke()\n    last.current = p\n  }\n\n  const clear = () => {\n    const c = canvas.current\n    if (c) c.getContext('2d')!.clearRect(0, 0, c.width, c.height)\n    setName('')\n    setEmpty(true)\n  }\n\n  const done = () => {\n    const s: Signature = mode === 'draw' ? { kind: 'drawn', png: canvas.current!.toDataURL('image/png') } : { kind: 'typed', name: name.trim() }\n    setSigned(s)\n    onSign(s)\n  }\n\n  const ready = mode === 'draw' ? !empty : name.trim().length > 1\n  const ghost = 'inline-flex min-h-10 items-center gap-1.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground'\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={{ type: 'spring', bounce: 0, duration: 0.5 }}>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        {signed ? (\n          <motion.div\n            key=\"signed\"\n            initial={{ opacity: 0, scale: 0.94 }}\n            animate={{ opacity: 1, scale: 1 }}\n            className={cn('flex items-center gap-2', className)}\n            role=\"status\"\n          >\n            <span className=\"inline-flex min-h-11 items-center gap-2 rounded-full bg-foreground px-5 font-medium text-background\">\n              <Check className=\"size-4\" aria-hidden />\n              {t.signed}\n            </span>\n            <button\n              type=\"button\"\n              onClick={() => {\n                setSigned(null)\n                setEmpty(true)\n              }}\n              aria-label={mode === 'draw' ? t.draw : t.type}\n              className=\"grid size-11 place-items-center rounded-full bg-muted transition-colors duration-150 hover:bg-accent\"\n            >\n              <PenLine className=\"size-4\" aria-hidden />\n            </button>\n          </motion.div>\n        ) : (\n          <motion.section\n            key=\"pad\"\n            initial={{ opacity: 0, y: 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -8 }}\n            aria-label={t.title}\n            className={cn('w-full max-w-sm rounded-3xl border bg-card p-4 text-card-foreground shadow-sm', className)}\n          >\n            <header className=\"mb-3 flex items-center justify-between\">\n              <h2 className=\"font-medium\">{t.title}</h2>\n              <button type=\"button\" className={ghost} onClick={() => setMode(mode === 'draw' ? 'type' : 'draw')}>\n                {mode === 'draw' ? <Keyboard className=\"size-4\" aria-hidden /> : <PenLine className=\"size-4\" aria-hidden />}\n                {mode === 'draw' ? t.type : t.draw}\n              </button>\n            </header>\n\n            {mode === 'draw' ? (\n              <canvas\n                ref={canvas}\n                role=\"img\"\n                aria-label={t.pad}\n                onPointerDown={down}\n                onPointerMove={move}\n                onPointerUp={() => (last.current = null)}\n                onPointerCancel={() => (last.current = null)}\n                className=\"h-44 w-full cursor-crosshair touch-none rounded-2xl border border-dashed bg-background text-foreground\"\n              />\n            ) : (\n              <label className=\"block\">\n                <span className=\"sr-only\">{t.typed}</span>\n                <input\n                  autoFocus\n                  value={name}\n                  onChange={(e) => setName(e.target.value)}\n                  placeholder={t.typed}\n                  autoComplete=\"name\"\n                  className=\"grid h-44 w-full rounded-2xl border border-dashed bg-background px-4 text-center font-serif text-3xl italic outline-none placeholder:text-base placeholder:not-italic placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                />\n              </label>\n            )}\n\n            <footer className=\"mt-3 flex items-center justify-between\">\n              <button type=\"button\" className={ghost} onClick={clear}>\n                <Eraser className=\"size-4\" aria-hidden />\n                {t.clear}\n              </button>\n              <button\n                type=\"button\"\n                disabled={!ready}\n                onClick={done}\n                className=\"inline-flex min-h-11 items-center gap-2 rounded-full bg-foreground px-5 text-sm font-medium text-background transition-[opacity,transform] duration-150 active:scale-[0.97] disabled:opacity-30\"\n              >\n                <Check className=\"size-4\" aria-hidden />\n                {t.done}\n              </button>\n            </footer>\n          </motion.section>\n        )}\n      </AnimatePresence>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "credit": "Adapted from Watermelon UI (MIT)."
  },
  "categories": [
    "controls"
  ],
  "type": "registry:component"
}