{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/signature-pad/SignaturePad.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface SignaturePadActions {\n    clear: () => void\n    exportSignature: () => void\n    empty: boolean\n  }\n\n  /** Imperative handle — grab it with `bind:this`. React's `SignaturePadRef`\n   *  exposes `pointCount` / `isEmpty` as render-time snapshot properties;\n   *  Svelte's `bind:this` publishes functions instead, so the live values are\n   *  read via `getPointCount()` / `isEmpty()`. `toDataURL()` additionally\n   *  returns the exported value (React's is a void alias of `exportSignature`). */\n  export interface SignaturePadRef {\n    clear: () => void\n    exportSignature: () => void\n    toDataURL: () => string | null\n    getPointCount: () => number\n    isEmpty: () => boolean\n  }\n\n  export interface SignaturePadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {\n    /** PNG data URL of the signature. Binds two-way; null when empty. */\n    value?: string | null\n    /** Reactive empty flag. `bind:empty` to observe — the component writes it\n     *  as ink is added/cleared; writing it from the outside does not repaint\n     *  the canvas (same one-way caveat as `value`). */\n    empty?: boolean\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    onbegin?: () => void\n    onend?: () => void\n    onchange?: (value: string | null) => void\n    /** Custom action row. Receives clear / exportSignature / empty. */\n    actions?: Snippet<[SignaturePadActions]>\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Eraser } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { signaturePadVariants } from './signature-pad.variants'\n\n  let {\n    class: className,\n    value = $bindable(null),\n    empty = $bindable(true),\n    width = 400,\n    height = 200,\n    // Theme tokens are OKLCH (`--foreground` / `--background`). Resolve via\n    // getComputedStyle — canvas cannot parse `hsl(var(--…))` or bare `var(--…)`.\n    penColor = '',\n    penThickness = 2,\n    backgroundColor = '',\n    exportFormat = 'image/png',\n    disabled = false,\n    readonly = false,\n    showClearButton = true,\n    clearLabel = 'Clear',\n    onbegin,\n    onend,\n    onchange,\n    actions,\n    ref = $bindable(null),\n    ...restProps\n  }: SignaturePadProps = $props()\n\n  let canvasRef: HTMLCanvasElement | null = $state(null)\n  let isDrawing = $state(false)\n  let pointCount = $state(0)\n  let hasInk = $state(false)\n  let ctx: CanvasRenderingContext2D | null = $state(null)\n  let lastX = 0\n  let lastY = 0\n\n  const isInteractive = $derived(!disabled && !readonly)\n\n  /** Resolve theme CSS variables (and legacy `hsl(var(--x))`) to a paint color canvas accepts. */\n  function resolveColor(input: string | undefined, cssVar: string): string {\n    let candidate = (input || '').trim()\n    if (!candidate) candidate = `var(${cssVar})`\n    // Legacy shadcn hsl() wrapper around a CSS variable — unwrap.\n    const hslWrapped = candidate.match(/^hsl\\(\\s*(var\\(--[^)]+\\))\\s*\\)$/i)\n    if (hslWrapped) candidate = hslWrapped[1]!\n    if (candidate.startsWith('var(')) {\n      const name = candidate.match(/var\\((--[^),]+)/)?.[1]\n      if (name && typeof document !== 'undefined') {\n        const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()\n        if (v) return v\n      }\n    }\n    return candidate\n  }\n\n  function setupCanvas() {\n    const canvas = canvasRef\n    if (!canvas || typeof window === 'undefined') 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 = resolveColor(penColor, '--foreground')\n    context.lineWidth = penThickness\n    context.fillStyle = resolveColor(backgroundColor, '--background')\n    context.fillRect(0, 0, width, height)\n    ctx = context\n    pointCount = 0\n    hasInk = false\n    empty = true\n  }\n\n  // Runs on mount and re-runs when pen/size options change (same trigger set\n  // as the Vue twin's watcher). Reads of width/height/colors subscribe the effect.\n  $effect(() => {\n    setupCanvas()\n    return () => {\n      ctx = null\n    }\n  })\n\n  function getPointerPos(e: PointerEvent): { x: number; y: number } {\n    const canvas = canvasRef\n    if (!canvas) return { x: 0, y: 0 }\n    const rect = canvas.getBoundingClientRect()\n    // Map CSS pixels → logical canvas coords if the element is CSS-scaled.\n    const scaleX = width / (rect.width || width)\n    const scaleY = height / (rect.height || height)\n    return {\n      x: (e.clientX - rect.left) * scaleX,\n      y: (e.clientY - rect.top) * scaleY,\n    }\n  }\n\n  function startDraw(e: PointerEvent) {\n    if (!isInteractive || !ctx) return\n    e.preventDefault()\n    isDrawing = true\n    const { x, y } = getPointerPos(e)\n    lastX = x\n    lastY = y\n    ctx.beginPath()\n    ctx.moveTo(x, y)\n    pointCount += 1\n    hasInk = true\n    empty = false\n    onbegin?.()\n    canvasRef?.setPointerCapture(e.pointerId)\n  }\n\n  function draw(e: PointerEvent) {\n    if (!isDrawing || !ctx) return\n    e.preventDefault()\n    const { x, y } = getPointerPos(e)\n    ctx.beginPath()\n    ctx.moveTo(lastX, lastY)\n    ctx.lineTo(x, y)\n    ctx.stroke()\n    lastX = x\n    lastY = y\n    pointCount += 1\n  }\n\n  function endDraw(e: PointerEvent) {\n    if (!isDrawing) return\n    isDrawing = false\n    ctx?.closePath()\n    canvasRef?.releasePointerCapture(e.pointerId)\n    exportSignature()\n    onend?.()\n  }\n\n  export function exportSignature() {\n    const canvas = canvasRef\n    if (!canvas) return\n    if (!hasInk) {\n      value = null\n      onchange?.(null)\n      return\n    }\n    const dataUrl = canvas.toDataURL(exportFormat)\n    value = dataUrl\n    onchange?.(dataUrl)\n  }\n\n  export function toDataURL() {\n    exportSignature()\n    return value\n  }\n\n  export function clear() {\n    const canvas = canvasRef\n    const context = ctx\n    if (!canvas || !context) return\n    context.fillStyle = resolveColor(backgroundColor, '--background')\n    context.fillRect(0, 0, width, height)\n    pointCount = 0\n    hasInk = false\n    empty = true\n    value = null\n    onchange?.(null)\n  }\n\n  export function isEmpty() {\n    return !hasInk\n  }\n\n  export function getPointCount() {\n    return pointCount\n  }\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"signature-pad\"\n  data-disabled={disabled ? '' : undefined}\n  data-readonly={readonly ? '' : undefined}\n  class={cn(signaturePadVariants(), className)}\n  {...restProps}\n>\n  <!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role, a11y_role_supports_aria_props -- canvas-as-img + aria-disabled mirror the Vue twin: a labeled drawing surface, announced state for disabled/readonly. -->\n  <canvas\n    bind:this={canvasRef}\n    class={cn('block touch-none rounded-md', !isInteractive && 'pointer-events-none')}\n    style=\"touch-action:none\"\n    aria-label={'Signature pad' + (disabled ? ' (disabled)' : readonly ? ' (readonly)' : '')}\n    aria-disabled={disabled || undefined}\n    role=\"img\"\n    onpointerdown={startDraw}\n    onpointermove={draw}\n    onpointerup={endDraw}\n    onpointercancel={endDraw}\n    onpointerleave={endDraw}\n  ></canvas>\n  {#if showClearButton && isInteractive}\n    <div class=\"flex items-center justify-between gap-2 pt-2\">\n      <span class=\"text-muted-foreground text-xs tabular-nums\">{pointCount} points</span>\n      <button\n        type=\"button\"\n        class=\"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 class=\"size-4\" />\n        {clearLabel}\n      </button>\n    </div>\n  {/if}\n  {@render actions?.({ clear, exportSignature, empty: !hasInk })}\n</div>\n\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/signature-pad/SignaturePad.svelte"
    },
    {
      "path": "packages/registry-svelte/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": "~/src/lib/components/ui/signature-pad/signature-pad.variants.ts"
    },
    {
      "path": "packages/registry-svelte/components/signature-pad/index.ts",
      "content": "export {\n  default as SignaturePad,\n  type SignaturePadActions,\n  type SignaturePadProps,\n  type SignaturePadRef,\n} from './SignaturePad.svelte'\nexport { signaturePadVariants, type SignaturePadVariants } from './signature-pad.variants'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/signature-pad/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte",
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Canvas-based digital signature capture with pointer events (mouse + touch). bind:value emits a PNG data URL, with pen color/thickness, background, clear method, disabled/readonly states, and a live point count.",
  "categories": [
    "control",
    "form"
  ]
}