{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sign-pad",
  "title": "Sign Pad",
  "description": "A signature pad with pressure-aware ink, undo and clear, and cropped PNG/SVG exports.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/sign-pad.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Eraser, Signature } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface SignPoint {\n  x: number;\n  y: number;\n  w: number;\n}\n\nexport type SignStroke = SignPoint[];\n\nexport interface SignPadValue {\n  dataUrl: string;\n  svg: string;\n  width: number;\n  height: number;\n  strokes: SignStroke[];\n}\n\nexport interface SignPadLabels {\n  start: string;\n  finish: string;\n  done: string;\n  title: string;\n  hint: string;\n  tooShort: string;\n  edit: string;\n  undo: string;\n  clear: string;\n  close: string;\n}\n\nexport interface SignPadProps {\n  ref?: React.Ref<HTMLDivElement>;\n  onComplete?: (value: SignPadValue) => void;\n  onCancel?: () => void;\n  onChange?: (value: SignPadValue | null) => void;\n  defaultValue?: SignPadValue | null;\n  accent?: string;\n  strokeWidth?: number;\n  minInk?: number;\n  labels?: Partial<SignPadLabels>;\n  className?: string;\n}\n\nconst DEFAULT_LABELS: SignPadLabels = {\n  start: \"Start signing\",\n  finish: \"Done\",\n  done: \"Signing done\",\n  title: \"Sign\",\n  hint: \"Sign here\",\n  tooShort: \"That looks short. Sign a little more.\",\n  edit: \"Edit signature\",\n  undo: \"Undo last stroke\",\n  clear: \"Clear signature\",\n  close: \"Close\",\n};\n\ntype Phase = \"idle\" | \"signing\" | \"done\";\n\nconst f = (n: number) => Math.round(n * 100) / 100;\n\nfunction edge(pts: { x: number; y: number }[]) {\n  let d = \"\";\n\n  for (let i = 1; i < pts.length - 1; i++) {\n    const mx = (pts[i].x + pts[i + 1].x) / 2;\n    const my = (pts[i].y + pts[i + 1].y) / 2;\n\n    d += `Q${f(pts[i].x)} ${f(pts[i].y)} ${f(mx)} ${f(my)}`;\n  }\n\n  const last = pts[pts.length - 1];\n\n  return d + `L${f(last.x)} ${f(last.y)}`;\n}\n\nfunction outlinePath(stroke: SignStroke, s: number) {\n  const n = stroke.length;\n\n  if (n === 0) return \"\";\n\n  const P = stroke.map((p) => ({\n    x: p.x * s,\n    y: p.y * s,\n    r: Math.max((p.w * s) / 2, 0.3),\n  }));\n\n  if (n === 1) {\n    const { x, y, r } = P[0];\n\n    return `M${f(x - r)} ${f(y)}a${f(r)} ${f(r)} 0 1 0 ${f(\n      r * 2,\n    )} 0a${f(r)} ${f(r)} 0 1 0 ${f(-r * 2)} 0Z`;\n  }\n\n  const left: { x: number; y: number }[] = [];\n  const right: { x: number; y: number }[] = [];\n\n  for (let i = 0; i < n; i++) {\n    const a = P[Math.max(0, i - 1)];\n    const b = P[Math.min(n - 1, i + 1)];\n\n    const dx = b.x - a.x;\n    const dy = b.y - a.y;\n    const len = Math.hypot(dx, dy) || 1;\n\n    const nx = -dy / len;\n    const ny = dx / len;\n\n    left.push({\n      x: P[i].x + nx * P[i].r,\n      y: P[i].y + ny * P[i].r,\n    });\n\n    right.push({\n      x: P[i].x - nx * P[i].r,\n      y: P[i].y - ny * P[i].r,\n    });\n  }\n\n  const first = P[0];\n  const last = P[n - 1];\n  const rl = right[n - 1];\n  const l0 = left[0];\n\n  return (\n    `M${f(l0.x)} ${f(l0.y)}` +\n    edge(left) +\n    `A${f(last.r)} ${f(last.r)} 0 0 0 ${f(rl.x)} ${f(rl.y)}` +\n    edge(right.slice().reverse()) +\n    `A${f(first.r)} ${f(first.r)} 0 0 0 ${f(l0.x)} ${f(l0.y)}Z`\n  );\n}\n\nfunction inkLength(strokes: SignStroke[]) {\n  let total = 0;\n\n  for (const s of strokes) {\n    for (let i = 1; i < s.length; i++) {\n      total += Math.hypot(s[i].x - s[i - 1].x, s[i].y - s[i - 1].y);\n    }\n  }\n\n  return total;\n}\n\nfunction cloneStrokes(strokes: SignStroke[]) {\n  return strokes.map((s) => s.map((p) => ({ ...p })));\n}\n\nfunction exportSignature(strokes: SignStroke[], s: number, ink: string): SignPadValue {\n  let x0 = Infinity;\n  let y0 = Infinity;\n  let x1 = -Infinity;\n  let y1 = -Infinity;\n\n  for (const stroke of strokes) {\n    for (const p of stroke) {\n      const r = p.w / 2;\n\n      x0 = Math.min(x0, p.x - r);\n      y0 = Math.min(y0, p.y - r);\n      x1 = Math.max(x1, p.x + r);\n      y1 = Math.max(y1, p.y + r);\n    }\n  }\n\n  const pad = 10;\n\n  const width = Math.ceil((x1 - x0) * s + pad * 2);\n  const height = Math.ceil((y1 - y0) * s + pad * 2);\n\n  const ox = -x0 * s + pad;\n  const oy = -y0 * s + pad;\n\n  const d = strokes.map((st) => outlinePath(st, s)).join(\"\");\n\n  const svg =\n    `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${width} ${height}\" width=\"${width}\" height=\"${height}\">` +\n    `<path transform=\"translate(${f(ox)} ${f(oy)})\" d=\"${d}\" fill=\"${ink}\"/></svg>`;\n\n  const scale = 3;\n  const canvas = document.createElement(\"canvas\");\n\n  canvas.width = width * scale;\n  canvas.height = height * scale;\n\n  const ctx = canvas.getContext(\"2d\");\n\n  if (ctx) {\n    ctx.scale(scale, scale);\n    ctx.translate(ox, oy);\n    ctx.fillStyle = ink;\n\n    for (const st of strokes) {\n      ctx.fill(new Path2D(outlinePath(st, s)));\n    }\n  }\n\n  return {\n    dataUrl: canvas.toDataURL(\"image/png\"),\n    svg,\n    width,\n    height,\n    strokes: cloneStrokes(strokes),\n  };\n}\n\nfunction IconButton({\n  label,\n  className,\n  ...props\n}: React.ButtonHTMLAttributes<HTMLButtonElement> & {\n  label: string;\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      className={cn(\n        \"border-input bg-background text-muted-foreground inline-flex size-8 items-center justify-center rounded-md border outline-none\",\n        \"hover:bg-accent hover:text-foreground active:bg-muted\",\n        \"focus-visible:ring-ring focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ninterface PanelProps {\n  initial: SignStroke[];\n  ink: string;\n  accent: string;\n  strokeWidth: number;\n  minInk: number;\n  labels: SignPadLabels;\n  onCancel: () => void;\n  onFinish: (value: SignPadValue) => void;\n}\n\nfunction SigningPanel({\n  initial,\n  ink,\n  accent,\n  strokeWidth,\n  minInk,\n  labels,\n  onCancel,\n  onFinish,\n}: PanelProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const strokesRef = React.useRef<SignStroke[]>(cloneStrokes(initial));\n  const sizeRef = React.useRef({ w: 0, h: 0, dpr: 1 });\n  const drawingRef = React.useRef<{ id: number; t: number } | null>(null);\n  const rafRef = React.useRef<number | null>(null);\n\n  const [stats, setStats] = React.useState(() => ({\n    count: initial.length,\n    length: inkLength(initial),\n  }));\n\n  const sync = React.useCallback(() => {\n    setStats({\n      count: strokesRef.current.length,\n      length: inkLength(strokesRef.current),\n    });\n  }, []);\n\n  const redraw = React.useCallback(() => {\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n\n    if (!canvas || !ctx) return;\n\n    const { w, h, dpr } = sizeRef.current;\n\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    ctx.clearRect(0, 0, w, h);\n    ctx.fillStyle = ink;\n\n    for (const stroke of strokesRef.current) {\n      ctx.fill(new Path2D(outlinePath(stroke, w)));\n    }\n  }, [ink]);\n\n  const schedule = React.useCallback(() => {\n    if (rafRef.current != null) return;\n\n    rafRef.current = requestAnimationFrame(() => {\n      rafRef.current = null;\n      redraw();\n      sync();\n    });\n  }, [redraw, sync]);\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n\n    if (!canvas) return;\n\n    const fit = () => {\n      const w = canvas.offsetWidth;\n      const h = canvas.offsetHeight;\n      const dpr = Math.min(window.devicePixelRatio || 1, 3);\n\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n\n      sizeRef.current = { w, h, dpr };\n\n      redraw();\n    };\n\n    fit();\n\n    const ro = new ResizeObserver(fit);\n    ro.observe(canvas);\n\n    return () => ro.disconnect();\n  }, [redraw]);\n\n  React.useEffect(\n    () => () => {\n      if (rafRef.current != null) {\n        cancelAnimationFrame(rafRef.current);\n      }\n    },\n    [],\n  );\n\n  const toLocal = (e: { clientX: number; clientY: number }) => {\n    const c = canvasRef.current!;\n    const r = c.getBoundingClientRect();\n\n    const sx = r.width / (c.offsetWidth || 1) || 1;\n    const sy = r.height / (c.offsetHeight || 1) || 1;\n\n    return {\n      x: (e.clientX - r.left) / sx,\n      y: (e.clientY - r.top) / sy,\n    };\n  };\n\n  const addPoint = (ev: PointerEvent) => {\n    const stroke = strokesRef.current[strokesRef.current.length - 1];\n    const drawing = drawingRef.current;\n\n    if (!stroke || !drawing) return;\n\n    const { w } = sizeRef.current;\n    const p = toLocal(ev);\n    const last = stroke[stroke.length - 1];\n\n    const dist = Math.hypot(p.x - last.x * w, p.y - last.y * w);\n\n    if (dist < 1.2) return;\n\n    const dt = Math.max(2, ev.timeStamp - drawing.t);\n\n    drawing.t = ev.timeStamp;\n\n    const speed = dist / dt;\n\n    const target =\n      ev.pointerType === \"pen\" && ev.pressure > 0\n        ? strokeWidth * (0.55 + ev.pressure)\n        : strokeWidth * (1.45 - Math.min(1, speed / 2.4));\n\n    const width = last.w * w * 0.72 + target * 0.28;\n\n    stroke.push({\n      x: p.x / w,\n      y: p.y / w,\n      w: width / w,\n    });\n  };\n\n  const onPointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => {\n    if (e.pointerType === \"mouse\" && e.button !== 0) return;\n\n    e.currentTarget.setPointerCapture(e.pointerId);\n\n    const { w } = sizeRef.current;\n    const p = toLocal(e);\n\n    strokesRef.current.push([\n      {\n        x: p.x / w,\n        y: p.y / w,\n        w: (strokeWidth * 0.9) / w,\n      },\n    ]);\n\n    drawingRef.current = {\n      id: e.pointerId,\n      t: e.timeStamp,\n    };\n\n    schedule();\n  };\n\n  const onPointerMove = (e: React.PointerEvent<HTMLCanvasElement>) => {\n    if (!drawingRef.current || drawingRef.current.id !== e.pointerId) {\n      return;\n    }\n\n    const events = e.nativeEvent.getCoalescedEvents?.() ?? [];\n\n    (events.length ? events : [e.nativeEvent]).forEach(addPoint);\n\n    schedule();\n  };\n\n  const endStroke = (e: React.PointerEvent<HTMLCanvasElement>) => {\n    if (!drawingRef.current || drawingRef.current.id !== e.pointerId) {\n      return;\n    }\n\n    drawingRef.current = null;\n    schedule();\n  };\n\n  const undo = () => {\n    if (!strokesRef.current.length) return;\n\n    strokesRef.current.pop();\n    schedule();\n  };\n\n  const clear = () => {\n    if (!strokesRef.current.length) return;\n\n    strokesRef.current = [];\n    redraw();\n    sync();\n  };\n\n  const finish = () => {\n    if (inkLength(strokesRef.current) < minInk) return;\n\n    onFinish(exportSignature(strokesRef.current, sizeRef.current.w, ink));\n  };\n\n  const canFinish = stats.length >= minInk;\n\n  const fillPercent = Math.min(100, (stats.length / minInk) * 100);\n\n  const latest = React.useRef({\n    undo,\n    finish,\n    onCancel,\n  });\n\n  React.useLayoutEffect(() => {\n    latest.current = {\n      undo,\n      finish,\n      onCancel,\n    };\n  });\n\n  React.useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      const mod = e.metaKey || e.ctrlKey;\n\n      if (e.key === \"Escape\") {\n        e.preventDefault();\n        latest.current.onCancel();\n      } else if (mod && e.key.toLowerCase() === \"z\") {\n        e.preventDefault();\n        latest.current.undo();\n      } else if (mod && e.key === \"Enter\") {\n        e.preventDefault();\n        latest.current.finish();\n      }\n    };\n\n    window.addEventListener(\"keydown\", onKey);\n\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, []);\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={labels.title}\n      className=\"relative w-[17.5rem] max-w-[calc(100vw-2rem)]\"\n    >\n      <div className=\"border-input bg-background relative mx-1.5 mt-1 h-32 overflow-hidden rounded-lg border dark:bg-zinc-950\">\n        <canvas\n          ref={canvasRef}\n          role=\"img\"\n          aria-label=\"Signature area. Draw with your finger, pen or mouse.\"\n          onPointerDown={onPointerDown}\n          onPointerMove={onPointerMove}\n          onPointerUp={endStroke}\n          onPointerCancel={endStroke}\n          className=\"absolute inset-0 size-full cursor-crosshair touch-none\"\n        />\n      </div>\n\n      <div className=\"mx-1.5 mt-2 mb-1.5 flex gap-2\">\n        <button\n          type=\"button\"\n          onClick={finish}\n          disabled={!canFinish}\n          className=\"focus-visible:ring-ring focus-visible:ring-offset-background relative inline-flex h-9 flex-1 items-center justify-center gap-2 overflow-hidden rounded-md border border-black/10 px-4 text-sm font-medium text-black transition-all outline-none hover:brightness-110 focus-visible:ring-2 focus-visible:ring-offset-2 active:brightness-95 disabled:cursor-not-allowed disabled:opacity-40 dark:text-white\"\n          style={{\n            backgroundColor: accent,\n            backgroundImage: `linear-gradient(90deg, transparent 0%, transparent ${fillPercent}%, rgba(0,0,0,0.05) ${fillPercent}%, rgba(0,0,0,0.05) 100%)`,\n          }}\n        >\n          <Signature className=\"size-4\" strokeWidth={2} />\n          {labels.finish}\n        </button>\n\n        <IconButton\n          label={labels.clear}\n          onClick={clear}\n          disabled={stats.count === 0}\n          className=\"size-9 shrink-0\"\n        >\n          <Eraser className=\"size-4\" strokeWidth={2.1} />\n        </IconButton>\n      </div>\n    </div>\n  );\n}\n\nfunction SignPad({\n  ref,\n  onComplete,\n  onCancel,\n  onChange,\n  defaultValue = null,\n  accent = \"var(--brand, #e1e900)\",\n  strokeWidth = 3,\n  minInk = 0.3,\n  labels: labelOverrides,\n  className,\n}: SignPadProps) {\n  const labels = React.useMemo(\n    () => ({\n      ...DEFAULT_LABELS,\n      ...labelOverrides,\n    }),\n    [labelOverrides],\n  );\n\n  const [signature, setSignature] = React.useState<SignPadValue | null>(defaultValue);\n\n  const [phase, setPhase] = React.useState<Phase>(defaultValue ? \"done\" : \"idle\");\n\n  const [resolvedInk, setResolvedInk] = React.useState(\"#111827\");\n\n  React.useEffect(() => {\n    const root = document.documentElement;\n\n    const updateInk = () => {\n      setResolvedInk(root.classList.contains(\"dark\") ? \"#f5f5f5\" : \"#111827\");\n    };\n\n    updateInk();\n\n    const observer = new MutationObserver(updateInk);\n\n    observer.observe(root, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => observer.disconnect();\n  }, []);\n\n  const cancel = () => {\n    setPhase(signature ? \"done\" : \"idle\");\n    onCancel?.();\n  };\n\n  const finish = (value: SignPadValue) => {\n    setSignature(value);\n    setPhase(\"done\");\n    onChange?.(value);\n    onComplete?.(value);\n  };\n\n  return (\n    <div ref={ref} className={cn(\"text-foreground inline-flex items-center\", className)}>\n      {phase === \"idle\" && (\n        <button\n          type=\"button\"\n          onClick={() => setPhase(\"signing\")}\n          className=\"border-input bg-background text-foreground hover:bg-accent focus-visible:ring-ring inline-flex h-9 items-center justify-center gap-2 rounded-md border px-4 text-sm font-medium outline-none focus-visible:ring-1\"\n        >\n          <Signature className=\"size-4\" strokeWidth={2} />\n          {labels.start}\n        </button>\n      )}\n\n      {phase === \"signing\" && (\n        <SigningPanel\n          initial={signature?.strokes ?? []}\n          ink={resolvedInk}\n          strokeWidth={strokeWidth}\n          minInk={minInk}\n          accent={accent}\n          labels={labels}\n          onCancel={cancel}\n          onFinish={finish}\n        />\n      )}\n\n      {phase === \"done\" && (\n        <button\n          type=\"button\"\n          aria-label={labels.edit}\n          onClick={() => setPhase(\"signing\")}\n          className=\"border-input bg-background text-foreground hover:bg-accent focus-visible:ring-ring inline-flex h-9 items-center gap-2 rounded-md border px-4 text-sm font-medium outline-none focus-visible:ring-1\"\n        >\n          <Signature className=\"size-4\" strokeWidth={2} />\n          {labels.edit}\n        </button>\n      )}\n    </div>\n  );\n}\n\nexport { SignPad };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}