{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/signature-pad/SignaturePad.tsx",
      "content": "import * as React from 'react'\nimport { Eraser } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { signaturePadVariants } from './signature-pad.variants'\n\nexport interface SignaturePadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n  /** PNG data URL of the current canvas contents, or null when empty. The\n   *  React equivalent of the Vue `v-model` binding — pair with `onModelChange`. */\n  modelValue?: string | null\n  width?: number\n  height?: number\n  penColor?: string\n  penThickness?: number\n  backgroundColor?: string\n  exportFormat?: string\n  disabled?: boolean\n  readonly?: boolean\n  showClearButton?: boolean\n  clearLabel?: string\n  /** Fired with the new data URL (or null) whenever the signature changes. */\n  onModelChange?: (value: string | null) => void\n  /** Fired when a stroke begins. */\n  onBegin?: () => void\n  /** Fired when a stroke ends. */\n  onEnd?: () => void\n  /** Fired with the new data URL (or null) on every change (same payload as onModelChange). */\n  onChange?: (value: string | null) => void\n  /** Render-prop for custom action controls. Mirrors the Vue `actions` slot. */\n  actions?: (state: { clear: () => void; exportSignature: () => void; empty: boolean }) => React.ReactNode\n}\n\nexport interface SignaturePadRef {\n  clear: () => void\n  exportSignature: () => void\n  toDataURL: () => void\n  pointCount: number\n  isEmpty: boolean\n}\n\nconst SignaturePad = React.forwardRef<SignaturePadRef, SignaturePadProps>(\n  (\n    {\n      modelValue,\n      width = 400,\n      height = 200,\n      penColor = '#0a0a0a',\n      penThickness = 2,\n      backgroundColor = '#ffffff',\n      exportFormat = 'image/png',\n      disabled = false,\n      readonly = false,\n      showClearButton = true,\n      clearLabel = 'Clear',\n      className,\n      onModelChange,\n      onBegin,\n      onEnd,\n      onChange,\n      actions,\n      ...props\n    },\n    ref,\n  ) => {\n    const canvasRef = React.useRef<HTMLCanvasElement | null>(null)\n    const ctxRef = React.useRef<CanvasRenderingContext2D | null>(null)\n    const isDrawingRef = React.useRef(false)\n    const lastPosRef = React.useRef({ x: 0, y: 0 })\n\n    const [pointCount, setPointCount] = React.useState(0)\n    const [hasInk, setHasInk] = React.useState(false)\n\n    // Keep the latest callbacks without re-running the canvas setup effect.\n    const callbacksRef = React.useRef({ onModelChange, onBegin, onEnd, onChange })\n    callbacksRef.current = { onModelChange, onBegin, onEnd, onChange }\n\n    const isInteractive = !disabled && !readonly\n\n    const setupCanvas = React.useCallback(() => {\n      const canvas = canvasRef.current\n      if (!canvas) return\n      const dpr = window.devicePixelRatio || 1\n      canvas.width = width * dpr\n      canvas.height = height * dpr\n      canvas.style.width = `${width}px`\n      canvas.style.height = `${height}px`\n      const context = canvas.getContext('2d')\n      if (!context) return\n      context.scale(dpr, dpr)\n      context.lineCap = 'round'\n      context.lineJoin = 'round'\n      context.strokeStyle = penColor\n      context.lineWidth = penThickness\n      context.fillStyle = backgroundColor\n      context.fillRect(0, 0, width, height)\n      ctxRef.current = context\n      setPointCount(0)\n      setHasInk(false)\n    }, [width, height, penColor, penThickness, backgroundColor])\n\n    const getPointerPos = React.useCallback((e: React.PointerEvent<HTMLCanvasElement>) => {\n      const canvas = canvasRef.current\n      if (!canvas) return { x: 0, y: 0 }\n      const rect = canvas.getBoundingClientRect()\n      return { x: e.clientX - rect.left, y: e.clientY - rect.top }\n    }, [])\n\n    const exportSignature = React.useCallback(() => {\n      const canvas = canvasRef.current\n      if (!canvas) return\n      const { onModelChange, onChange } = callbacksRef.current\n      if (!hasInk) {\n        onModelChange?.(null)\n        onChange?.(null)\n        return\n      }\n      const dataUrl = canvas.toDataURL(exportFormat)\n      onModelChange?.(dataUrl)\n      onChange?.(dataUrl)\n    }, [exportFormat, hasInk])\n\n    const clear = React.useCallback(() => {\n      const canvas = canvasRef.current\n      const context = ctxRef.current\n      if (!canvas || !context) return\n      context.fillStyle = backgroundColor\n      context.fillRect(0, 0, width, height)\n      setPointCount(0)\n      setHasInk(false)\n      callbacksRef.current.onModelChange?.(null)\n      callbacksRef.current.onChange?.(null)\n    }, [backgroundColor, width, height])\n\n    React.useImperativeHandle(\n      ref,\n      (): SignaturePadRef => ({\n        clear,\n        exportSignature,\n        toDataURL: exportSignature,\n        pointCount,\n        isEmpty: !hasInk,\n      }),\n      [clear, exportSignature, pointCount, hasInk],\n    )\n\n    React.useEffect(() => {\n      setupCanvas()\n      return () => {\n        ctxRef.current = null\n      }\n    }, [setupCanvas])\n\n    // modelValue is read-only from the outside; we do not paint it back onto\n    // the canvas (mirrors the Vue component, which also never restores ink).\n\n    const handlePointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => {\n      if (!isInteractive || !ctxRef.current) return\n      e.preventDefault()\n      isDrawingRef.current = true\n      const { x, y } = getPointerPos(e)\n      lastPosRef.current = { x, y }\n      ctxRef.current.beginPath()\n      ctxRef.current.moveTo(x, y)\n      setPointCount((c) => c + 1)\n      setHasInk(true)\n      callbacksRef.current.onBegin?.()\n      canvasRef.current?.setPointerCapture(e.pointerId)\n    }\n\n    const handlePointerMove = (e: React.PointerEvent<HTMLCanvasElement>) => {\n      if (!isDrawingRef.current || !ctxRef.current) return\n      e.preventDefault()\n      const { x, y } = getPointerPos(e)\n      const { x: lastX, y: lastY } = lastPosRef.current\n      ctxRef.current.beginPath()\n      ctxRef.current.moveTo(lastX, lastY)\n      ctxRef.current.lineTo(x, y)\n      ctxRef.current.stroke()\n      lastPosRef.current = { x, y }\n      setPointCount((c) => c + 1)\n    }\n\n    const handlePointerUp = (e: React.PointerEvent<HTMLCanvasElement>) => {\n      if (!isDrawingRef.current) return\n      isDrawingRef.current = false\n      ctxRef.current?.closePath()\n      canvasRef.current?.releasePointerCapture(e.pointerId)\n      exportSignature()\n      callbacksRef.current.onEnd?.()\n    }\n\n    return (\n      <div\n        data-uipkge=\"\"\n        data-slot=\"signature-pad\"\n        data-disabled={disabled ? '' : undefined}\n        data-readonly={readonly ? '' : undefined}\n        className={cn(signaturePadVariants(), className)}\n        {...props}\n      >\n        <canvas\n          ref={canvasRef}\n          className={cn('block touch-none rounded-md', !isInteractive && 'pointer-events-none')}\n          style={{ touchAction: 'none' }}\n          aria-label={`Signature pad${disabled ? ' (disabled)' : ''}`}\n          role=\"img\"\n          onPointerDown={handlePointerDown}\n          onPointerMove={handlePointerMove}\n          onPointerUp={handlePointerUp}\n          onPointerCancel={handlePointerUp}\n          onPointerLeave={handlePointerUp}\n        />\n        {showClearButton && isInteractive ? (\n          <div className=\"flex items-center justify-between gap-2 pt-2\">\n            <span className=\"text-muted-foreground text-xs tabular-nums\">{pointCount} points</span>\n            <button\n              type=\"button\"\n              className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex items-center gap-1 rounded-md text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\"\n              disabled={!hasInk}\n              onClick={clear}\n            >\n              <Eraser className=\"size-3.5\" />\n              {clearLabel}\n            </button>\n          </div>\n        ) : null}\n        {actions ? actions({ clear, exportSignature, empty: !hasInk }) : null}\n      </div>\n    )\n  },\n)\nSignaturePad.displayName = 'SignaturePad'\n\nexport { SignaturePad }\n",
      "type": "registry:ui",
      "target": "~/components/ui/signature-pad/SignaturePad.tsx"
    },
    {
      "path": "packages/registry-react/components/signature-pad/signature-pad.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\nexport const signaturePadVariants = cva(\n  'border-input bg-background inline-flex flex-col gap-1 rounded-lg border p-2 shadow-xs',\n)\n\nexport type SignaturePadVariants = VariantProps<typeof signaturePadVariants>\n",
      "type": "registry:ui",
      "target": "~/components/ui/signature-pad/signature-pad.variants.ts"
    },
    {
      "path": "packages/registry-react/components/signature-pad/index.ts",
      "content": "export { SignaturePad, type SignaturePadProps, type SignaturePadRef } from './SignaturePad'\nexport { signaturePadVariants, type SignaturePadVariants } from './signature-pad.variants'\n",
      "type": "registry:ui",
      "target": "~/components/ui/signature-pad/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Canvas-based digital signature capture with pointer events (mouse + touch). The value is a PNG data URL, with pen color/thickness, background, clear method, disabled/readonly states, and a live point count.",
  "categories": [
    "control",
    "form"
  ]
}