{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A responsive pointer signature surface with a full keyboard-accessible typed alternative and undo.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/signature-pad.tsx",
      "content": "// Ballmac UI: Signature Pad. https://ui.ballmac.com/components/signature-pad\n\"use client\"\n\nimport * as React from \"react\"\nimport { PenLine, RotateCcw, Trash2, Type } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype SignatureValue =\n  | {\n      /** Drawing mode. */ mode: \"draw\"\n      /** SVG path strings in a 600 × 200 viewBox. */ strokes: string[]\n    }\n  | { /** Typing mode. */ mode: \"type\"; /** Typed signer name. */ text: string }\ntype SignaturePadProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> & {\n  /** Visible and accessible field label. */\n  label: string\n  /** Controlled signature. */\n  value?: SignatureValue\n  /** Initial signature. */\n  defaultValue?: SignatureValue\n  /** Called when a stroke, typed name, or mode changes. */\n  onValueChange?: (value: SignatureValue) => void\n  /** Native form field name for serialized signature data. */\n  name?: string\n  /** Disable drawing, typing, and actions. */\n  disabled?: boolean\n}\nfunction SignaturePad({\n  className,\n  label,\n  value,\n  defaultValue = { mode: \"draw\", strokes: [] },\n  onValueChange,\n  name,\n  disabled = false,\n  ...props\n}: SignaturePadProps) {\n  const msg = useMessages()\n  const [internal, setInternal] = React.useState<SignatureValue>(defaultValue)\n  const [preview, setPreview] = React.useState<string | null>(null)\n  const active = React.useRef<string | null>(null)\n  const current = value ?? internal\n  function update(next: SignatureValue) {\n    if (value === undefined) setInternal(next)\n    onValueChange?.(next)\n  }\n  function point(event: React.PointerEvent<SVGSVGElement>) {\n    const rect = event.currentTarget.getBoundingClientRect()\n    return [\n      Math.max(\n        0,\n        Math.min(600, ((event.clientX - rect.left) / rect.width) * 600),\n      ),\n      Math.max(\n        0,\n        Math.min(200, ((event.clientY - rect.top) / rect.height) * 200),\n      ),\n    ]\n  }\n  function begin(event: React.PointerEvent<SVGSVGElement>) {\n    if (disabled || current.mode !== \"draw\") return\n    const [x, y] = point(event)\n    active.current = `M ${x.toFixed(1)} ${y.toFixed(1)}`\n    setPreview(active.current)\n    event.currentTarget.setPointerCapture?.(event.pointerId)\n  }\n  function draw(event: React.PointerEvent<SVGSVGElement>) {\n    if (!active.current) return\n    const [x, y] = point(event)\n    active.current += ` L ${x.toFixed(1)} ${y.toFixed(1)}`\n    setPreview(active.current)\n  }\n  function end() {\n    if (!active.current || current.mode !== \"draw\") return\n    const stroke = active.current.includes(\" L \")\n      ? active.current\n      : `${active.current} l 0.1 0`\n    update({ mode: \"draw\", strokes: [...current.strokes, stroke] })\n    active.current = null\n    setPreview(null)\n  }\n  function clear() {\n    update(\n      current.mode === \"draw\"\n        ? { mode: \"draw\", strokes: [] }\n        : { mode: \"type\", text: \"\" },\n    )\n  }\n  const hasValue =\n    current.mode === \"draw\"\n      ? current.strokes.length > 0\n      : current.text.length > 0\n  return (\n    <div\n      data-slot=\"signature-pad\"\n      className={cn(\n        \"bg-card min-w-0 rounded-xl border border-border p-4\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex items-center justify-between gap-3\">\n        <span className=\"text-sm font-medium\">{label}</span>\n        <span className=\"text-muted-foreground text-xs\">{msg(\"signature-pad.drawOrType\", \"Draw or type\")}</span>\n      </div>\n      <div\n        role=\"group\"\n        aria-label={msg(\"signature-pad.signatureMethod\", \"Signature method\")}\n        className=\"bg-muted mt-3 inline-flex rounded-lg p-1\"\n      >\n        <button\n          type=\"button\"\n          disabled={disabled}\n          aria-pressed={current.mode === \"draw\"}\n          onClick={() => update({ mode: \"draw\", strokes: [] })}\n          className={cn(\n            \"inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs font-medium outline-none transition-colors duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50 motion-reduce:transition-none\",\n            current.mode === \"draw\"\n              ? \"bg-background text-foreground shadow-sm\"\n              : \"text-muted-foreground hover:text-foreground\",\n          )}\n        >\n          <PenLine aria-hidden=\"true\" className=\"size-3.5\" />\n          {msg(\"signature-pad.draw\", \"Draw\")}\n        </button>\n        <button\n          type=\"button\"\n          disabled={disabled}\n          aria-pressed={current.mode === \"type\"}\n          onClick={() => update({ mode: \"type\", text: \"\" })}\n          className={cn(\n            \"inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs font-medium outline-none transition-colors duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50 motion-reduce:transition-none\",\n            current.mode === \"type\"\n              ? \"bg-background text-foreground shadow-sm\"\n              : \"text-muted-foreground hover:text-foreground\",\n          )}\n        >\n          <Type aria-hidden=\"true\" className=\"size-3.5\" />\n          {msg(\"signature-pad.type\", \"Type\")}\n        </button>\n      </div>\n      {current.mode === \"draw\" ? (\n        <div className=\"border-input bg-background mt-3 overflow-hidden rounded-md border\">\n          <svg\n            data-slot=\"signature-pad-canvas\"\n            role=\"img\"\n            aria-label={msg(\"signature-pad.drawingArea\", \"{label} drawing area\", { label })}\n            viewBox=\"0 0 600 200\"\n            preserveAspectRatio=\"none\"\n            onPointerDown={begin}\n            onPointerMove={draw}\n            onPointerUp={end}\n            onPointerCancel={() => {\n              active.current = null\n              setPreview(null)\n            }}\n            className={cn(\n              \"text-foreground aspect-[3/1] w-full touch-none\",\n              disabled && \"pointer-events-none opacity-50\",\n            )}\n          >\n            <path\n              d=\"M 0 160 H 600\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeOpacity=\"0.16\"\n              strokeWidth=\"1\"\n              strokeDasharray=\"4 5\"\n            />\n            {current.strokes.map((stroke, index) => (\n              <path\n                key={index}\n                d={stroke}\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2.5\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            ))}\n            {preview && (\n              <path\n                d={preview}\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2.5\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            )}\n          </svg>\n        </div>\n      ) : (\n        <input\n          data-slot=\"signature-pad-type\"\n          type=\"text\"\n          value={current.text}\n          aria-label={msg(\"signature-pad.typeLabel\", \"Type {label}\", { label: label.toLowerCase() })}\n          placeholder={msg(\"signature-pad.typeYourFullName\", \"Type your full name\")}\n          disabled={disabled}\n          onChange={(event) =>\n            update({ mode: \"type\", text: event.target.value })\n          }\n          className=\"border-input bg-background mt-3 h-20 w-full rounded-md border px-4 font-serif text-2xl italic outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50\"\n        />\n      )}\n      <div className=\"mt-3 flex items-center justify-between gap-3\">\n        <p className=\"text-muted-foreground text-xs\">\n          {current.mode === \"draw\"\n            ? \"Use a pointer to sign; choose Type for keyboard entry.\"\n            : \"Your typed name is saved as the signature.\"}\n        </p>\n        <div className=\"flex shrink-0 gap-1\">\n          {current.mode === \"draw\" && (\n            <button\n              type=\"button\"\n              aria-label={msg(\"signature-pad.undoLastStroke\", \"Undo last stroke\")}\n              disabled={disabled || !hasValue}\n              onClick={() =>\n                update({ mode: \"draw\", strokes: current.strokes.slice(0, -1) })\n              }\n              className=\"text-muted-foreground hover:bg-accent flex size-8 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40\"\n            >\n              <RotateCcw aria-hidden=\"true\" className=\"size-4\" />\n            </button>\n          )}\n          <button\n            type=\"button\"\n            aria-label={msg(\"signature-pad.clearSignature\", \"Clear signature\")}\n            disabled={disabled || !hasValue}\n            onClick={clear}\n            className=\"text-muted-foreground hover:bg-accent flex size-8 items-center justify-center rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-40\"\n          >\n            <Trash2 aria-hidden=\"true\" className=\"size-4\" />\n          </button>\n        </div>\n      </div>\n      {name && (\n        <input type=\"hidden\" name={name} value={JSON.stringify(current)} />\n      )}\n    </div>\n  )\n}\nexport { SignaturePad, type SignaturePadProps, type SignatureValue }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/signature-pad.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "signature",
      "drawing",
      "form"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "A responsive pointer signature surface with a full keyboard-accessible typed alternative and undo.",
      "whenToUse": [
        "Capture a signer name or freehand mark"
      ],
      "whenNotToUse": [
        "Use a text input when a signature is not required"
      ],
      "composesWith": [
        "input"
      ],
      "a11y": [
        {
          "keys": "Tab / Enter / Space / typing",
          "action": "Switches methods, types a name, and clears or undoes the signature"
        }
      ],
      "customization": [
        "Controlled and uncontrolled value",
        "Tokens and state styling"
      ]
    },
    "examples": [
      "signature-pad-demo",
      "signature-pad-states"
    ],
    "url": "https://ui.ballmac.com/components/signature-pad"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}