{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "type": "registry:component",
  "title": "Signature Pad",
  "description": "A place to sign with a finger, mouse or pen: strokes come out smooth and swell with pressure or slow down, with undo, a typed fallback and a stamped, timestamped signed state.",
  "categories": [
    "component",
    "Forms"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/signature-pad.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { BadgeCheckIcon, EraserIcon, PenLineIcon, Undo2Icon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ntype Point = { x: number; y: number; w: number };\ntype Stroke = Point[];\n\nconst MIN_W = 1.1;\nconst MAX_W = 3.4;\n\nfunction drawStroke(ctx: CanvasRenderingContext2D, stroke: Stroke, from = 0) {\n  if (stroke.length === 1) {\n    const p = stroke[0];\n    ctx.beginPath();\n    ctx.arc(p.x, p.y, p.w / 2, 0, Math.PI * 2);\n    ctx.fill();\n    return;\n  }\n  for (let i = Math.max(1, from); i < stroke.length; i++) {\n    const a = stroke[i - 1];\n    const b = stroke[i];\n    const before = stroke[i - 2] ?? a;\n    const start = { x: (before.x + a.x) / 2, y: (before.y + a.y) / 2 };\n    const end = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };\n    ctx.beginPath();\n    ctx.moveTo(i === 1 ? a.x : start.x, i === 1 ? a.y : start.y);\n    ctx.quadraticCurveTo(a.x, a.y, end.x, end.y);\n    ctx.lineWidth = (a.w + b.w) / 2;\n    ctx.stroke();\n  }\n}\n\nexport interface SignaturePadProps {\n  signer: string;\n  label?: string;\n  onSign?: (png: string, signedAt: Date) => void;\n  onClear?: () => void;\n  className?: string;\n}\n\nexport function SignaturePad({ signer, label = \"Signature\", onSign, onClear, className }: SignaturePadProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const boxRef = useRef<HTMLDivElement>(null);\n  const strokes = useRef<Stroke[]>([]);\n  const typedInk = useRef<{ text: string; font: string; x: number; y: number } | null>(null);\n  const typedRef = useRef<HTMLInputElement>(null);\n  const live = useRef<{ stroke: Stroke; t: number; id: number } | null>(null);\n  const [count, setCount] = useState(0);\n  const [signedAt, setSignedAt] = useState<Date | null>(null);\n  const [mode, setMode] = useState<\"draw\" | \"type\">(\"draw\");\n  const [typed, setTyped] = useState(signer);\n  const reduce = useReducedMotion();\n  const id = useId();\n\n  const ctx = useCallback(() => {\n    const canvas = canvasRef.current;\n    const c = canvas?.getContext(\"2d\");\n    if (!canvas || !c) return null;\n    const color = getComputedStyle(canvas).color;\n    c.strokeStyle = color;\n    c.fillStyle = color;\n    c.lineCap = \"round\";\n    c.lineJoin = \"round\";\n    return c;\n  }, []);\n\n  const redraw = useCallback(() => {\n    const canvas = canvasRef.current;\n    const c = ctx();\n    if (!canvas || !c) return;\n    c.clearRect(0, 0, canvas.width, canvas.height);\n    for (const s of strokes.current) drawStroke(c, s);\n    if (typedInk.current) {\n      c.font = typedInk.current.font;\n      c.textBaseline = \"middle\";\n      c.fillText(typedInk.current.text, typedInk.current.x, typedInk.current.y);\n    }\n  }, [ctx]);\n\n  useLayoutEffect(() => {\n    const canvas = canvasRef.current;\n    const box = boxRef.current;\n    if (!canvas || !box) return;\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 3);\n      canvas.width = Math.round(box.clientWidth * dpr);\n      canvas.height = Math.round(box.clientHeight * dpr);\n      canvas.getContext(\"2d\")?.setTransform(dpr, 0, 0, dpr, 0, 0);\n      redraw();\n    };\n    resize();\n    const sizes = new ResizeObserver(resize);\n    sizes.observe(box);\n    const theme = new MutationObserver(redraw);\n    theme.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\"] });\n    return () => {\n      sizes.disconnect();\n      theme.disconnect();\n    };\n  }, [redraw]);\n\n  const onSignRef = useRef(onSign);\n  onSignRef.current = onSign;\n  useEffect(() => {\n    redraw();\n    if (!signedAt) return;\n    const png = canvasRef.current?.toDataURL(\"image/png\");\n    if (png) onSignRef.current?.(png, signedAt);\n  }, [signedAt, redraw]);\n\n  const point = (\n    canvas: HTMLCanvasElement,\n    e: { clientX: number; clientY: number; pointerType: string; pressure: number },\n    prev?: Point,\n    dt = 16,\n  ): Point => {\n    const rect = canvas.getBoundingClientRect();\n    const x = e.clientX - rect.left;\n    const y = e.clientY - rect.top;\n    let target: number;\n    if (e.pointerType === \"pen\" && e.pressure > 0) {\n      target = MIN_W + (MAX_W - MIN_W) * Math.min(e.pressure * 1.4, 1);\n    } else {\n      const speed = prev ? Math.hypot(x - prev.x, y - prev.y) / Math.max(dt, 1) : 0;\n      target = Math.max(MIN_W, MAX_W - speed * 1.5);\n    }\n    return { x, y, w: prev ? prev.w * 0.65 + target * 0.35 : target * 0.8 };\n  };\n\n  const onPointerDown = (event: PointerEvent<HTMLCanvasElement>) => {\n    if (signedAt || event.button !== 0) return;\n    event.currentTarget.setPointerCapture(event.pointerId);\n    const stroke: Stroke = [point(event.currentTarget, event)];\n    live.current = { stroke, t: performance.now(), id: event.pointerId };\n    strokes.current.push(stroke);\n    const c = ctx();\n    if (c) drawStroke(c, stroke);\n  };\n\n  const onPointerMove = (event: PointerEvent<HTMLCanvasElement>) => {\n    const l = live.current;\n    if (!l || l.id !== event.pointerId) return;\n    const events = event.nativeEvent.getCoalescedEvents?.() ?? [event.nativeEvent];\n    const c = ctx();\n    for (const e of events) {\n      const now = performance.now();\n      const prev = l.stroke[l.stroke.length - 1];\n      const p = point(event.currentTarget, e, prev, now - l.t);\n      if (Math.hypot(p.x - prev.x, p.y - prev.y) < 1.2) continue;\n      l.t = now;\n      l.stroke.push(p);\n      if (c) drawStroke(c, l.stroke, l.stroke.length - 1);\n    }\n  };\n\n  const onPointerUp = () => {\n    if (!live.current) return;\n    live.current = null;\n    setCount(strokes.current.length);\n  };\n\n  const undo = () => {\n    strokes.current.pop();\n    setCount(strokes.current.length);\n    redraw();\n  };\n\n  const clear = () => {\n    strokes.current = [];\n    typedInk.current = null;\n    setMode(\"draw\");\n    setCount(0);\n    setSignedAt(null);\n    redraw();\n    onClear?.();\n  };\n\n  const sign = () => {\n    if (mode === \"type\") {\n      strokes.current = [];\n      const input = typedRef.current;\n      const box = boxRef.current?.getBoundingClientRect();\n      if (input && box) {\n        const style = getComputedStyle(input);\n        const r = input.getBoundingClientRect();\n        typedInk.current = {\n          text: typed.trim(),\n          font: `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`,\n          x: r.left - box.left + Number.parseFloat(style.paddingLeft) - 1,\n          y: r.top - box.top + r.height / 2 + 2,\n        };\n      }\n    }\n    setSignedAt(new Date());\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === \"z\" && count && !signedAt) {\n      event.preventDefault();\n      undo();\n    }\n  };\n\n  const empty = mode === \"draw\" ? count === 0 : !typed.trim();\n  const when = signedAt\n    ? new Intl.DateTimeFormat(\"en-GB\", { day: \"numeric\", month: \"short\", year: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" }).format(signedAt)\n    : \"\";\n\n  return (\n    <div className={cn(\"grid w-full gap-2\", className)} onKeyDown={onKeyDown}>\n      <div className=\"flex items-baseline justify-between gap-2\">\n        <span className=\"font-medium text-sm\" id={`${id}-label`}>\n          {label}\n        </span>\n        {!signedAt && (\n          <button\n            className=\"cursor-pointer rounded-sm text-muted-foreground text-xs underline-offset-4 outline-none hover:text-foreground hover:underline focus-visible:ring-2 focus-visible:ring-ring\"\n            onClick={() => {\n              setMode((m) => (m === \"draw\" ? \"type\" : \"draw\"));\n            }}\n            type=\"button\"\n          >\n            {mode === \"draw\" ? \"Type it instead\" : \"Draw it instead\"}\n          </button>\n        )}\n      </div>\n      <div\n        className={cn(\n          \"relative h-40 overflow-hidden rounded-xl border bg-background not-dark:bg-clip-padding shadow-xs/5 ring-ring/24 transition-[border-color,box-shadow] duration-300 has-focus-visible:border-ring has-focus-visible:ring-[3px] dark:bg-input/32\",\n          signedAt ? \"border-info/40\" : \"border-input\",\n        )}\n        ref={boxRef}\n      >\n        <div aria-hidden className=\"pointer-events-none absolute inset-x-5 bottom-9 flex items-end gap-2 text-muted-foreground/60\">\n          <span className=\"-mb-1 text-base leading-none\">×</span>\n          <span className=\"h-px flex-1 bg-[repeating-linear-gradient(to_right,currentColor_0_6px,transparent_6px_10px)]\" />\n        </div>\n        <AnimatePresence>\n          {empty && mode === \"draw\" && (\n            <motion.span\n              animate={{ opacity: 1 }}\n              aria-hidden\n              className=\"pointer-events-none absolute inset-x-0 top-12 flex items-center justify-center gap-1.5 text-muted-foreground text-sm\"\n              exit={{ opacity: 0, transition: { duration: 0.12 } }}\n              initial={{ opacity: 0 }}\n            >\n              <PenLineIcon className=\"size-4\" />\n              Sign with a finger, mouse or pen\n            </motion.span>\n          )}\n        </AnimatePresence>\n        <canvas\n          aria-label={signedAt ? `Signature by ${signer}, signed ${when}` : `Signature pad for ${signer}${empty ? \", empty\" : \"\"}`}\n          className={cn(\n            \"absolute inset-0 size-full touch-none\",\n            signedAt ? \"cursor-default text-info-foreground\" : \"cursor-crosshair text-foreground\",\n            mode === \"type\" && !signedAt && \"pointer-events-none opacity-0\",\n          )}\n          onPointerCancel={onPointerUp}\n          onPointerDown={onPointerDown}\n          onPointerMove={onPointerMove}\n          onPointerUp={onPointerUp}\n          ref={canvasRef}\n          role=\"img\"\n        />\n        {mode === \"type\" && !signedAt && (\n          <input\n            aria-label=\"Type your full name\"\n            autoFocus\n            className=\"absolute inset-x-5 bottom-10 h-10 pl-5 bg-transparent font-serif text-3xl text-foreground italic outline-none placeholder:text-muted-foreground/40\"\n            onChange={(event) => setTyped(event.target.value)}\n            placeholder=\"Your full name\"\n            ref={typedRef}\n            value={typed}\n          />\n        )}\n        <AnimatePresence>\n          {signedAt && (\n            <motion.div\n              animate={{ opacity: 1, scale: 1, rotate: -4 }}\n              className=\"pointer-events-none absolute top-3 right-3 flex items-center gap-1 rounded-md border border-info/30 bg-info/8 px-2 py-1 font-medium text-[11px] text-info-foreground uppercase tracking-wide\"\n              exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.12 } }}\n              initial={reduce ? { opacity: 0, rotate: -4 } : { opacity: 0, scale: 1.25, rotate: -4 }}\n              transition={{ type: \"spring\", stiffness: 520, damping: 26 }}\n            >\n              <BadgeCheckIcon className=\"size-3.5\" />\n              Signed\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n      <div className=\"flex min-h-8 items-center justify-between gap-2\">\n        {signedAt ? (\n          <p className=\"truncate text-muted-foreground text-xs tabular-nums\">\n            Signed by <span className=\"text-foreground\">{signer}</span> · {when}\n          </p>\n        ) : (\n          <div className=\"flex items-center gap-1\">\n            <Button aria-label=\"Undo last stroke\" disabled={count === 0 || mode === \"type\"} onClick={undo} size=\"icon-sm\" variant=\"ghost\">\n              <Undo2Icon />\n            </Button>\n            <Button aria-label=\"Clear signature\" disabled={count === 0 || mode === \"type\"} onClick={clear} size=\"icon-sm\" variant=\"ghost\">\n              <EraserIcon />\n            </Button>\n          </div>\n        )}\n        {signedAt ? (\n          <Button onClick={clear} size=\"sm\" variant=\"ghost\">\n            Sign again\n          </Button>\n        ) : (\n          <Button disabled={empty} onClick={sign} size=\"sm\">\n            Adopt and sign\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}