{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "description": "Capture a drawn or typed signature and export it as SVG or a PNG data URL.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "input",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/signature-pad.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { EraserIcon, KeyboardIcon, PenLineIcon, Undo2Icon } from \"lucide-react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  exportSignature,\n  nameFontSize,\n  PAD_HEIGHT,\n  PAD_WIDTH,\n  PEN_WIDTH,\n  SIGNATURE_BASELINE,\n  signatureImageSrc,\n  strokeToPath,\n  type SignatureFormat,\n  type SignatureInk,\n  type SignaturePoint,\n  type SignatureStroke,\n} from \"@/registry/default/lib/signature-pad\"\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Input } from \"@/registry/default/ui/input\"\n\nfunction SignaturePad({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  format = \"svg\",\n  penColor = \"currentColor\",\n  name,\n  placeholder = \"Sign here\",\n  disabled = false,\n  className,\n  \"aria-label\": ariaLabel = \"Signature\",\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  format?: SignatureFormat\n  penColor?: string\n  name?: string\n  placeholder?: string\n  disabled?: boolean\n}) {\n  const { value: signature, setValue } = useControllableState({\n    value,\n    defaultValue,\n    onChange: onValueChange,\n  })\n  const [strokes, setStrokes] = React.useState<SignatureStroke[]>([])\n  const [typing, setTyping] = React.useState(false)\n  const [typedName, setTypedName] = React.useState(\"\")\n  const [activePointer, setActivePointer] = React.useState<number | null>(null)\n  const [exported, setExported] = React.useState(signature)\n  const surfaceRef = React.useRef<SVGSVGElement>(null)\n  const draftRef = React.useRef<SVGPathElement>(null)\n  const draftStroke = React.useRef<SignatureStroke>([])\n\n  if (signature !== exported) {\n    setExported(signature)\n    setStrokes([])\n    setTypedName(\"\")\n    setTyping(false)\n  }\n\n  const trimmedName = typedName.trim()\n  const hasInk = typing ? trimmedName.length > 0 : strokes.length > 0\n  const saved = signature !== \"\" && !hasInk\n  const showPlaceholder = !hasInk && !saved && activePointer === null\n\n  function commit(ink: SignatureInk) {\n    const color =\n      format === \"svg\" && penColor === \"currentColor\"\n        ? penColor\n        : getComputedStyle(surfaceRef.current!).color\n    const next = exportSignature(ink, format, color)\n    setExported(next)\n    setValue(next)\n  }\n\n  function toPoint(event: { clientX: number; clientY: number }) {\n    const rect = surfaceRef.current!.getBoundingClientRect()\n    return [\n      ((event.clientX - rect.left) / rect.width) * PAD_WIDTH,\n      ((event.clientY - rect.top) / rect.height) * PAD_HEIGHT,\n    ] satisfies SignaturePoint\n  }\n\n  function handlePointerDown(event: React.PointerEvent<SVGSVGElement>) {\n    if (typing || saved || disabled) return\n    if (event.button !== 0 || activePointer !== null) return\n    event.currentTarget.setPointerCapture(event.pointerId)\n    draftStroke.current = [toPoint(event)]\n    draftRef.current?.setAttribute(\"d\", strokeToPath(draftStroke.current))\n    setActivePointer(event.pointerId)\n  }\n\n  function handlePointerMove(event: React.PointerEvent<SVGSVGElement>) {\n    if (event.pointerId !== activePointer) return\n    const samples = event.nativeEvent.getCoalescedEvents?.() ?? []\n    const moves = samples.length > 0 ? samples : [event]\n    draftStroke.current.push(...moves.map(toPoint))\n    draftRef.current?.setAttribute(\"d\", strokeToPath(draftStroke.current))\n  }\n\n  function handlePointerEnd(event: React.PointerEvent<SVGSVGElement>) {\n    if (event.pointerId !== activePointer) return\n    const next = [...strokes, draftStroke.current]\n    draftRef.current?.removeAttribute(\"d\")\n    setActivePointer(null)\n    setStrokes(next)\n    commit(next)\n  }\n\n  function undo() {\n    const next = strokes.slice(0, -1)\n    setStrokes(next)\n    commit(next)\n  }\n\n  function clear() {\n    setStrokes([])\n    setTypedName(\"\")\n    commit([])\n  }\n\n  function toggleMode() {\n    setTyping(!typing)\n    commit(typing ? strokes : trimmedName)\n  }\n\n  const inertClassName =\n    \"aria-disabled:pointer-events-none aria-disabled:opacity-50\"\n\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"signature-pad\"\n      aria-label={ariaLabel}\n      aria-disabled={disabled || undefined}\n      className={cn(\"grid w-full gap-2\", className)}\n      {...props}\n    >\n      <div\n        data-slot=\"signature-pad-surface\"\n        className={cn(\n          \"relative overflow-hidden rounded-md border border-input bg-transparent shadow-xs dark:bg-input/30\",\n          disabled && \"opacity-50\"\n        )}\n      >\n        <svg\n          ref={surfaceRef}\n          role=\"img\"\n          aria-label={\n            hasInk || saved ? `${ariaLabel} added` : `${ariaLabel} empty`\n          }\n          viewBox={`0 0 ${PAD_WIDTH} ${PAD_HEIGHT}`}\n          style={{ color: penColor }}\n          onPointerDown={handlePointerDown}\n          onPointerMove={handlePointerMove}\n          onPointerUp={handlePointerEnd}\n          onPointerCancel={handlePointerEnd}\n          className={cn(\n            \"block aspect-3/1 w-full touch-none select-none\",\n            !typing && !saved && !disabled && \"cursor-crosshair\"\n          )}\n        >\n          <line\n            x1={24}\n            x2={PAD_WIDTH - 24}\n            y1={SIGNATURE_BASELINE + 8}\n            y2={SIGNATURE_BASELINE + 8}\n            className=\"stroke-border\"\n          />\n          {typing ? (\n            <text\n              x={PAD_WIDTH / 2}\n              y={SIGNATURE_BASELINE}\n              textAnchor=\"middle\"\n              fontFamily=\"cursive\"\n              fontStyle=\"italic\"\n              fontSize={nameFontSize(trimmedName)}\n              fill=\"currentColor\"\n            >\n              {trimmedName}\n            </text>\n          ) : (\n            <g\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={PEN_WIDTH}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              {strokes.map((stroke, index) => (\n                <path key={index} d={strokeToPath(stroke)} />\n              ))}\n              <path ref={draftRef} />\n            </g>\n          )}\n        </svg>\n        {saved && (\n          <div\n            data-slot=\"signature-pad-saved\"\n            className=\"pointer-events-none absolute inset-0\"\n            style={{\n              backgroundColor: penColor,\n              mask: `url(\"${signatureImageSrc(signature)}\") center / contain no-repeat`,\n            }}\n          />\n        )}\n        {showPlaceholder && (\n          <span className=\"pointer-events-none absolute bottom-1/4 left-6 text-sm text-muted-foreground\">\n            {placeholder}\n          </span>\n        )}\n      </div>\n      {typing && (\n        <Input\n          autoFocus\n          aria-label=\"Type your name\"\n          placeholder=\"Your full name\"\n          value={typedName}\n          disabled={disabled}\n          onChange={(event) => {\n            setTypedName(event.target.value)\n            commit(event.target.value.trim())\n          }}\n        />\n      )}\n      <div\n        data-slot=\"signature-pad-actions\"\n        className=\"flex items-center gap-1\"\n      >\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          disabled={disabled || saved}\n          onClick={toggleMode}\n        >\n          {typing ? <PenLineIcon /> : <KeyboardIcon />}\n          {typing ? \"Draw instead\" : \"Type instead\"}\n        </Button>\n        {!typing && (\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"sm\"\n            disabled={disabled}\n            aria-disabled={strokes.length === 0 || undefined}\n            onClick={() => strokes.length > 0 && undo()}\n            className={cn(\"ml-auto\", inertClassName)}\n          >\n            <Undo2Icon />\n            Undo\n          </Button>\n        )}\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          disabled={disabled}\n          aria-disabled={signature === \"\" || undefined}\n          onClick={() => signature !== \"\" && clear()}\n          className={cn(typing && \"ml-auto\", inertClassName)}\n        >\n          <EraserIcon />\n          Clear\n        </Button>\n      </div>\n      {name && (\n        <input\n          type=\"hidden\"\n          name={name}\n          disabled={disabled}\n          value={signature}\n        />\n      )}\n    </div>\n  )\n}\n\nexport { SignaturePad }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/signature-pad.ts",
      "content": "export type SignaturePoint = [x: number, y: number]\nexport type SignatureStroke = SignaturePoint[]\nexport type SignatureInk = SignatureStroke[] | string\nexport type SignatureFormat = \"svg\" | \"png\"\n\nexport const PAD_WIDTH = 600\nexport const PAD_HEIGHT = 200\nexport const PEN_WIDTH = 2.5\nexport const SIGNATURE_BASELINE = 150\n\nconst PNG_SCALE = 2\nconst NAME_MAX_FONT_SIZE = 56\nconst NAME_MAX_WIDTH = 560\nconst NAME_GLYPH_WIDTH = 0.55\n\nfunction round(value: number) {\n  return Math.round(value * 10) / 10\n}\n\nfunction escapeXml(text: string) {\n  return text.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`)\n}\n\nexport function nameFontSize(name: string) {\n  return Math.min(\n    NAME_MAX_FONT_SIZE,\n    round(NAME_MAX_WIDTH / (name.length * NAME_GLYPH_WIDTH))\n  )\n}\n\nexport function strokeToPath(stroke: SignatureStroke) {\n  const [[startX, startY], ...rest] = stroke\n  const start = `M${round(startX)} ${round(startY)}`\n  if (rest.length === 0) return `${start}l.01 0`\n\n  const curves = rest.slice(0, -1).map(([x, y], index) => {\n    const [nextX, nextY] = rest[index + 1]\n    return `Q${round(x)} ${round(y)} ${round((x + nextX) / 2)} ${round((y + nextY) / 2)}`\n  })\n  const [endX, endY] = rest[rest.length - 1]\n\n  return `${start}${curves.join(\"\")}L${round(endX)} ${round(endY)}`\n}\n\nfunction inkToSvg(ink: SignatureInk, color: string) {\n  const paint = escapeXml(color)\n  const body =\n    typeof ink === \"string\"\n      ? `<text x=\"${PAD_WIDTH / 2}\" y=\"${SIGNATURE_BASELINE}\" text-anchor=\"middle\" font-family=\"cursive\" font-style=\"italic\" font-size=\"${nameFontSize(ink)}\" fill=\"${paint}\">${escapeXml(ink)}</text>`\n      : `<g fill=\"none\" stroke=\"${paint}\" stroke-width=\"${PEN_WIDTH}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${ink\n          .map((stroke) => `<path d=\"${strokeToPath(stroke)}\"/>`)\n          .join(\"\")}</g>`\n\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${PAD_WIDTH}\" height=\"${PAD_HEIGHT}\" viewBox=\"0 0 ${PAD_WIDTH} ${PAD_HEIGHT}\">${body}</svg>`\n}\n\nfunction inkToPng(ink: SignatureInk, color: string) {\n  const canvas = document.createElement(\"canvas\")\n  canvas.width = PAD_WIDTH * PNG_SCALE\n  canvas.height = PAD_HEIGHT * PNG_SCALE\n\n  const context = canvas.getContext(\"2d\")!\n  context.scale(PNG_SCALE, PNG_SCALE)\n  context.fillStyle = color\n  context.strokeStyle = color\n  context.lineWidth = PEN_WIDTH\n  context.lineCap = \"round\"\n  context.lineJoin = \"round\"\n\n  if (typeof ink === \"string\") {\n    context.font = `italic ${nameFontSize(ink)}px cursive`\n    context.textAlign = \"center\"\n    context.fillText(ink, PAD_WIDTH / 2, SIGNATURE_BASELINE)\n  } else {\n    for (const stroke of ink) {\n      context.stroke(new Path2D(strokeToPath(stroke)))\n    }\n  }\n\n  return canvas.toDataURL(\"image/png\")\n}\n\nexport function exportSignature(\n  ink: SignatureInk,\n  format: SignatureFormat,\n  color: string\n) {\n  if (ink.length === 0) return \"\"\n  return format === \"png\" ? inkToPng(ink, color) : inkToSvg(ink, color)\n}\n\nexport function signatureImageSrc(value: string) {\n  return value.startsWith(\"data:\")\n    ? value\n    : `data:image/svg+xml,${encodeURIComponent(value)}`\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}