{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "type": "registry:ui",
  "title": "Signature Pad",
  "description": "Canvas signature capture with smooth, velocity-weighted strokes, pen pressure, undo, PNG / SVG export and a typed-name fallback.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/signature-pad.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/* -------------------------------------------------------------------------- */\n/*  Types                                                                      */\n/* -------------------------------------------------------------------------- */\n\ninterface StrokePoint {\n  /** CSS px within the pad at the time of capture. */\n  x: number;\n  y: number;\n  /** Line width in CSS px at this point (from velocity and pen pressure). */\n  w: number;\n  t: number;\n}\n\ninterface Stroke {\n  points: StrokePoint[];\n  /** Explicit ink, or null to follow the theme's text colour. */\n  color: string | null;\n  /** Pad size when the stroke was drawn; strokes are rescaled to fit on resize. */\n  size: [number, number];\n}\n\nexport interface SignatureData {\n  isEmpty: boolean;\n  mode: \"draw\" | \"type\";\n  /** PNG data URL with a transparent background, or null when empty. */\n  dataUrl: string | null;\n  /** Centre-line path data of all strokes (empty in typed mode). */\n  svgPath: string;\n  /** The typed name (typed mode). */\n  typedName: string;\n}\n\nexport interface SignaturePadHandle {\n  clear: () => void;\n  undo: () => void;\n  isEmpty: () => boolean;\n  /** PNG (or another image type) at 2× resolution, transparent background. Null when empty. */\n  toDataURL: (type?: string) => string | null;\n  /** Standalone SVG document; each stroke is drawn with its average width. */\n  toSVG: () => string;\n  /** Path data (\"M … Q …\") of all strokes, scaled to the current pad size. */\n  toSVGPath: () => string;\n}\n\nexport interface SignaturePadProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"children\"> {\n  ref?: React.Ref<SignaturePadHandle>;\n  /** Visible label and accessible name. */\n  label?: string;\n  /** Ink colour. Defaults to the theme's text colour (dark on light, light on dark). */\n  penColor?: string;\n  /** Maximum stroke width in px. */\n  strokeWidth?: number;\n  /** How much speed thins the line (0 = constant width, 1 = strong). */\n  thinning?: number;\n  /** Use pen pressure when the pointer is a stylus. */\n  pressure?: boolean;\n  /** Pad height in px (width follows the container). */\n  height?: number;\n  /** Baseline with a cross mark, plus the hint while empty. */\n  showGuide?: boolean;\n  /** Hint shown on the empty pad. */\n  hint?: string;\n  /** Offer a \"Type\" mode so keyboard users can sign by typing their name. */\n  allowTypedFallback?: boolean;\n  /** Focus ring and active mode colour. */\n  accent?: string;\n  /** Called after every stroke, undo, clear or typed change. */\n  onChange?: (data: SignatureData) => void;\n  disabled?: boolean;\n  /** Submits the PNG data URL with a form under this name. */\n  name?: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Drawing helpers                                                            */\n/* -------------------------------------------------------------------------- */\n\nconst TYPED_FONT = '\"Segoe Script\", \"Brush Script MT\", \"Snell Roundhand\", \"Apple Chancery\", cursive';\nconst clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));\n\nlet probe: CanvasRenderingContext2D | null = null;\n/** Resolves any CSS colour (including Tailwind's oklch) to rgb() by painting a 1×1 canvas. */\nfunction resolveColor(input: string, fallback: string) {\n  probe ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!probe) return fallback;\n  probe.clearRect(0, 0, 1, 1);\n  probe.fillStyle = \"rgba(0, 0, 0, 0)\";\n  probe.fillStyle = input;\n  probe.fillRect(0, 0, 1, 1);\n  const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;\n  return a === 0 ? fallback : `rgb(${r}, ${g}, ${b})`;\n}\n\n/** Uniform scale that fits a stroke captured at `size` into the current pad. */\nconst fitScale = (size: [number, number], width: number, height: number) =>\n  Math.min(width / (size[0] || 1), height / (size[1] || 1));\n\nconst mid = (a: StrokePoint, b: StrokePoint) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });\n\n/**\n * Draws the part of a stroke that ends at point `i`: quadratic curves through the midpoints of\n * consecutive points, each with the width of its control point. Calling it for every new point\n * draws the stroke incrementally; calling it for all points redraws it.\n */\nfunction drawSegment(ctx: CanvasRenderingContext2D, pts: StrokePoint[], i: number, s: number, final: boolean) {\n  if (i === 0) {\n    if (final && pts.length === 1) {\n      ctx.beginPath();\n      ctx.arc(pts[0].x * s, pts[0].y * s, (pts[0].w * s) / 2, 0, Math.PI * 2);\n      ctx.fill();\n    }\n    return;\n  }\n  ctx.beginPath();\n  if (i === 1) {\n    const m = mid(pts[0], pts[1]);\n    ctx.moveTo(pts[0].x * s, pts[0].y * s);\n    ctx.lineTo(m.x * s, m.y * s);\n    ctx.lineWidth = pts[0].w * s;\n  } else {\n    const a = mid(pts[i - 2], pts[i - 1]);\n    const b = mid(pts[i - 1], pts[i]);\n    ctx.moveTo(a.x * s, a.y * s);\n    ctx.quadraticCurveTo(pts[i - 1].x * s, pts[i - 1].y * s, b.x * s, b.y * s);\n    ctx.lineWidth = pts[i - 1].w * s;\n  }\n  ctx.stroke();\n  if (final) {\n    // Close the gap between the last midpoint and the final point.\n    const last = pts[pts.length - 1];\n    const m = mid(pts[pts.length - 2], last);\n    ctx.beginPath();\n    ctx.moveTo(m.x * s, m.y * s);\n    ctx.lineTo(last.x * s, last.y * s);\n    ctx.lineWidth = last.w * s;\n    ctx.stroke();\n  }\n}\n\nfunction drawStroke(ctx: CanvasRenderingContext2D, stroke: Stroke, ink: string, width: number, height: number) {\n  const s = fitScale(stroke.size, width, height);\n  ctx.strokeStyle = ink;\n  ctx.fillStyle = ink;\n  ctx.lineCap = \"round\";\n  ctx.lineJoin = \"round\";\n  const pts = stroke.points;\n  for (let i = 0; i < pts.length; i++) drawSegment(ctx, pts, i, s, i === pts.length - 1);\n}\n\nfunction strokePath(stroke: Stroke, width: number, height: number) {\n  const s = fitScale(stroke.size, width, height);\n  const f = (n: number) => (n * s).toFixed(1);\n  const pts = stroke.points;\n  if (pts.length === 1) return `M${f(pts[0].x)} ${f(pts[0].y)}h0`;\n  let d = `M${f(pts[0].x)} ${f(pts[0].y)}`;\n  for (let i = 1; i < pts.length - 1; i++) {\n    const m = mid(pts[i], pts[i + 1]);\n    d += `Q${f(pts[i].x)} ${f(pts[i].y)} ${f(m.x)} ${f(m.y)}`;\n  }\n  const last = pts[pts.length - 1];\n  return `${d}L${f(last.x)} ${f(last.y)}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Component                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A canvas signature pad with smooth, velocity-weighted strokes and pen pressure. Strokes are\n * stored as points and redrawn on resize, DPR changes and theme switches. Export PNG or SVG via\n * the ref or `onChange`. Drawing needs a pointer, so a typed-name fallback is offered for\n * keyboard and assistive-technology users.\n */\nexport function SignaturePad({\n  ref,\n  label = \"Signature\",\n  penColor,\n  strokeWidth = 2.5,\n  thinning = 0.6,\n  pressure = true,\n  height = 200,\n  showGuide = true,\n  hint = \"Sign here\",\n  allowTypedFallback = true,\n  accent = \"#FF7448\",\n  onChange,\n  disabled = false,\n  name,\n  className = \"\",\n  style,\n  onKeyDown,\n  ...props\n}: SignaturePadProps) {\n  const id = React.useId();\n  const labelId = `${id}-label`;\n  const noteId = `${id}-note`;\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const strokes = React.useRef<Stroke[]>([]);\n  const current = React.useRef<{ stroke: Stroke; velocity: number; pointerId: number } | null>(null);\n  const size = React.useRef<[number, number]>([0, 0]);\n  const [strokeCount, setStrokeCount] = React.useState(0);\n  const [mode, setMode] = React.useState<\"draw\" | \"type\">(\"draw\");\n  const [typedName, setTypedName] = React.useState(\"\");\n  const [status, setStatus] = React.useState(\"\");\n  const [exported, setExported] = React.useState(\"\");\n\n  const typing = allowTypedFallback && mode === \"type\";\n  const empty = typing ? typedName.trim() === \"\" : strokeCount === 0;\n\n  const inkFor = (stroke: Stroke | null) => {\n    const canvas = canvasRef.current;\n    const explicit = stroke?.color ?? penColor;\n    if (explicit) return explicit;\n    return canvas ? resolveColor(getComputedStyle(canvas).color, \"rgb(24, 24, 27)\") : \"rgb(24, 24, 27)\";\n  };\n\n  /** Clears the canvas and redraws every stored stroke at the current size and DPR. */\n  const redraw = () => {\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!canvas || !ctx) return;\n    const [w, h] = size.current;\n    ctx.setTransform(1, 0, 0, 1, 0, 0);\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    const dpr = canvas.width / Math.max(w, 1);\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    for (const stroke of strokes.current) drawStroke(ctx, stroke, inkFor(stroke), w, h);\n  };\n  const redrawRef = React.useRef(redraw);\n  React.useEffect(() => {\n    redrawRef.current = redraw;\n  });\n\n  // Ink colour or width props changed: repaint with the stored points.\n  React.useEffect(() => {\n    redrawRef.current();\n  }, [penColor, mode]);\n\n  // Size the backing store to CSS size × DPR (capped at 2) and redraw on resize.\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ro = new ResizeObserver(([entry]) => {\n      const { width, height: h } = entry.contentRect;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      size.current = [width, h];\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      redrawRef.current();\n    });\n    ro.observe(canvas);\n    // Theme switches change the default ink: repaint when <html> attributes or the scheme change.\n    const mo = new MutationObserver(() => redrawRef.current());\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\"] });\n    const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    const onScheme = () => redrawRef.current();\n    mq.addEventListener(\"change\", onScheme);\n    return () => {\n      ro.disconnect();\n      mo.disconnect();\n      mq.removeEventListener(\"change\", onScheme);\n    };\n  }, [mode]);\n\n  /* --------------------------------- export ---------------------------------- */\n\n  const toDataURL = (type = \"image/png\", forceTyped?: string): string | null => {\n    const [w, h] = size.current;\n    const typed = forceTyped ?? (typing ? typedName.trim() : \"\");\n    if (!w || !h || (typing ? !typed : strokes.current.length === 0)) return null;\n    const out = document.createElement(\"canvas\");\n    out.width = Math.round(w * 2);\n    out.height = Math.round(h * 2);\n    const ctx = out.getContext(\"2d\");\n    if (!ctx) return null;\n    ctx.scale(2, 2);\n    if (typing) {\n      ctx.fillStyle = inkFor(null);\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"alphabetic\";\n      let fontSize = h * 0.3;\n      ctx.font = `${fontSize}px ${TYPED_FONT}`;\n      const measured = ctx.measureText(typed).width;\n      if (measured > w * 0.86) {\n        fontSize *= (w * 0.86) / measured;\n        ctx.font = `${fontSize}px ${TYPED_FONT}`;\n      }\n      ctx.fillText(typed, w / 2, h * 0.7);\n    } else {\n      for (const stroke of strokes.current) drawStroke(ctx, stroke, inkFor(stroke), w, h);\n    }\n    return out.toDataURL(type);\n  };\n\n  const toSVGPath = () => {\n    const [w, h] = size.current;\n    return strokes.current.map((s) => strokePath(s, w, h)).join(\"\");\n  };\n\n  const toSVG = () => {\n    const [w, h] = size.current;\n    const paths = strokes.current\n      .map((stroke) => {\n        const s = fitScale(stroke.size, w, h);\n        const avg = stroke.points.reduce((sum, p) => sum + p.w, 0) / stroke.points.length;\n        return `<path d=\"${strokePath(stroke, w, h)}\" stroke=\"${inkFor(stroke)}\" stroke-width=\"${(avg * s).toFixed(2)}\"/>`;\n      })\n      .join(\"\");\n    return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${Math.round(w)} ${Math.round(h)}\" width=\"${Math.round(w)}\" height=\"${Math.round(h)}\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${paths}</svg>`;\n  };\n\n  const notify = (nextMode: \"draw\" | \"type\", nextTyped: string) => {\n    const isTyped = allowTypedFallback && nextMode === \"type\";\n    const isEmpty = isTyped ? nextTyped.trim() === \"\" : strokes.current.length === 0;\n    if (!onChange && name === undefined) return;\n    const dataUrl = isEmpty ? null : isTyped ? toDataURL(\"image/png\", nextTyped.trim()) : toDataURL();\n    if (name !== undefined) setExported(dataUrl ?? \"\");\n    onChange?.({ isEmpty, mode: isTyped ? \"type\" : \"draw\", dataUrl, svgPath: isTyped ? \"\" : toSVGPath(), typedName: nextTyped });\n  };\n\n  const clear = () => {\n    strokes.current = [];\n    setStrokeCount(0);\n    setTypedName(\"\");\n    redraw();\n    setStatus(\"Signature cleared\");\n    notify(mode, \"\");\n  };\n\n  const undo = () => {\n    if (!strokes.current.length) return;\n    strokes.current = strokes.current.slice(0, -1);\n    setStrokeCount(strokes.current.length);\n    redraw();\n    setStatus(strokes.current.length ? `Last stroke removed, ${strokes.current.length} left` : \"Signature cleared\");\n    notify(mode, typedName);\n  };\n\n  React.useImperativeHandle(ref, () => ({\n    clear,\n    undo,\n    isEmpty: () => (typing ? typedName.trim() === \"\" : strokes.current.length === 0),\n    toDataURL: (type?: string) => toDataURL(type),\n    toSVG,\n    toSVGPath,\n  }));\n\n  /* ---------------------------------- input ---------------------------------- */\n\n  const addPoint = (event: PointerEvent | React.PointerEvent, rect: DOMRect) => {\n    const c = current.current;\n    if (!c) return;\n    const pts = c.stroke.points;\n    const x = event.clientX - rect.left;\n    const y = event.clientY - rect.top;\n    const t = event.timeStamp;\n    const prev = pts[pts.length - 1];\n    if (prev && Math.hypot(x - prev.x, y - prev.y) < 0.6) return;\n    // Faster movement → thinner line; velocity is smoothed so widths change gradually.\n    if (prev) {\n      const v = Math.hypot(x - prev.x, y - prev.y) / Math.max(t - prev.t, 1);\n      c.velocity = c.velocity * 0.7 + v * 0.3;\n    }\n    let target = strokeWidth * (1 - clamp(thinning, 0, 1) * clamp(c.velocity / 2.5, 0, 0.85));\n    if (pressure && event.pointerType === \"pen\" && event.pressure > 0) target *= 0.35 + event.pressure * 1.1;\n    const w = prev ? prev.w * 0.6 + target * 0.4 : target;\n    pts.push({ x, y, w, t });\n    const ctx = canvasRef.current?.getContext(\"2d\");\n    if (ctx) {\n      ctx.strokeStyle = ctx.fillStyle = inkFor(c.stroke);\n      ctx.lineCap = \"round\";\n      ctx.lineJoin = \"round\";\n      drawSegment(ctx, pts, pts.length - 1, 1, false);\n    }\n  };\n\n  const finishStroke = () => {\n    const c = current.current;\n    if (!c) return;\n    current.current = null;\n    const ctx = canvasRef.current?.getContext(\"2d\");\n    const pts = c.stroke.points;\n    if (ctx && pts.length) {\n      ctx.strokeStyle = ctx.fillStyle = inkFor(c.stroke);\n      drawSegment(ctx, pts, pts.length - 1, 1, true);\n    }\n    strokes.current = [...strokes.current, c.stroke];\n    setStrokeCount(strokes.current.length);\n    setStatus(`Signature captured, ${strokes.current.length} ${strokes.current.length === 1 ? \"stroke\" : \"strokes\"}`);\n    notify(\"draw\", typedName);\n  };\n\n  const modeButton = (value: \"draw\" | \"type\", text: string) => (\n    <button\n      type=\"button\"\n      aria-pressed={mode === value}\n      onClick={() => {\n        setMode(value);\n        setStatus(value === \"type\" ? \"Type mode: enter your name\" : \"Draw mode\");\n        notify(value, typedName);\n      }}\n      className={`rounded-md px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.12em] outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--sp-accent)] ${\n        mode === value\n          ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\"\n          : \"text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100\"\n      }`}\n    >\n      {text}\n    </button>\n  );\n\n  const actionButton =\n    \"rounded-md px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-zinc-600 outline-none transition-colors hover:bg-zinc-900/[0.06] hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-[var(--sp-accent)] disabled:pointer-events-none disabled:opacity-35 dark:text-zinc-300 dark:hover:bg-white/[0.08] dark:hover:text-white\";\n\n  return (\n    <div\n      role=\"group\"\n      aria-labelledby={labelId}\n      className={`flex w-full flex-col gap-2 ${disabled ? \"pointer-events-none opacity-50\" : \"\"} ${className}`}\n      style={{ [\"--sp-accent\" as string]: accent, ...style }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        // Ctrl / Cmd + Z undoes the last stroke while focus is inside the pad.\n        if (!typing && (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === \"z\" && !(event.target instanceof HTMLInputElement)) {\n          event.preventDefault();\n          undo();\n        }\n      }}\n      {...props}\n    >\n      <div className=\"flex items-center justify-between gap-3\">\n        <span id={labelId} className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n          {label}\n        </span>\n        {allowTypedFallback && (\n          <div role=\"group\" aria-label=\"Signature method\" className=\"flex gap-1\">\n            {modeButton(\"draw\", \"Draw\")}\n            {modeButton(\"type\", \"Type\")}\n          </div>\n        )}\n      </div>\n\n      <div\n        className=\"relative overflow-hidden rounded-xl border border-zinc-900/10 bg-white dark:border-white/10 dark:bg-zinc-900\"\n        style={{ height }}\n      >\n        {showGuide && (\n          <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-5 bottom-[28%] flex items-end gap-2\">\n            <span className=\"mb-1 font-mono text-sm leading-none text-zinc-400 dark:text-zinc-500\">×</span>\n            <span className=\"h-px flex-1 bg-zinc-900/15 dark:bg-white/15\" />\n          </div>\n        )}\n        {showGuide && empty && !typing && (\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-x-0 top-[36%] text-center font-mono text-[10px] uppercase tracking-[0.18em] text-zinc-400 transition-opacity duration-200 motion-reduce:transition-none dark:text-zinc-500\"\n          >\n            {hint}\n          </span>\n        )}\n\n        {typing ? (\n          <div className=\"absolute inset-0 flex flex-col\">\n            <label htmlFor={`${id}-typed`} className=\"sr-only\">\n              Type your full name\n            </label>\n            <input\n              id={`${id}-typed`}\n              type=\"text\"\n              autoComplete=\"name\"\n              placeholder=\"Type your full name\"\n              value={typedName}\n              onChange={(event) => {\n                setTypedName(event.target.value);\n                notify(\"type\", event.target.value);\n              }}\n              className=\"mx-5 mt-4 h-9 rounded-md border border-zinc-900/10 bg-transparent px-3 text-sm text-zinc-900 outline-none placeholder:text-zinc-400 focus-visible:border-[var(--sp-accent)] dark:border-white/10 dark:text-zinc-100 dark:placeholder:text-zinc-500\"\n            />\n            <p\n              aria-hidden=\"true\"\n              className=\"absolute inset-x-6 bottom-[28%] mb-1 truncate text-center text-[clamp(1.5rem,6vw,2.75rem)] leading-none text-zinc-900 dark:text-zinc-100\"\n              style={{ fontFamily: TYPED_FONT, color: penColor }}\n            >\n              {typedName}\n            </p>\n          </div>\n        ) : (\n          <canvas\n            ref={canvasRef}\n            role=\"img\"\n            aria-label={empty ? `${label} area, empty` : `${label} area, ${strokeCount} ${strokeCount === 1 ? \"stroke\" : \"strokes\"} drawn`}\n            aria-describedby={noteId}\n            className={`absolute inset-0 size-full touch-none text-zinc-900 dark:text-zinc-100 ${disabled ? \"cursor-not-allowed\" : \"cursor-crosshair\"}`}\n            onPointerDown={(event) => {\n              if (disabled || (event.pointerType === \"mouse\" && event.button !== 0)) return;\n              event.preventDefault();\n              event.currentTarget.setPointerCapture(event.pointerId);\n              current.current = { stroke: { points: [], color: penColor ?? null, size: [size.current[0], size.current[1]] }, velocity: 0, pointerId: event.pointerId };\n              addPoint(event, event.currentTarget.getBoundingClientRect());\n            }}\n            onPointerMove={(event) => {\n              if (current.current?.pointerId !== event.pointerId) return;\n              const rect = event.currentTarget.getBoundingClientRect();\n              // Coalesced events give the full-resolution path between frames.\n              const events = event.nativeEvent.getCoalescedEvents?.() ?? [];\n              if (events.length) for (const e of events) addPoint(e, rect);\n              else addPoint(event, rect);\n            }}\n            onPointerUp={finishStroke}\n            onPointerCancel={finishStroke}\n            onLostPointerCapture={finishStroke}\n          />\n        )}\n      </div>\n\n      <div className=\"flex items-center justify-between gap-3\">\n        <p id={noteId} className=\"min-w-0 text-[11px] leading-snug text-zinc-500 dark:text-zinc-400\">\n          {typing\n            ? \"Your typed name is used as your signature.\"\n            : allowTypedFallback\n              ? \"Draw with a mouse, finger or pen, or choose Type.\"\n              : \"Draw with a mouse, finger or pen.\"}\n        </p>\n        <div className=\"flex shrink-0 gap-1\">\n          {!typing && (\n            <button type=\"button\" className={actionButton} disabled={strokeCount === 0} onClick={undo}>\n              Undo\n            </button>\n          )}\n          <button type=\"button\" className={actionButton} disabled={empty} onClick={clear}>\n            Clear\n          </button>\n        </div>\n      </div>\n\n      <p role=\"status\" className=\"sr-only\">\n        {status}\n      </p>\n      {name !== undefined && <input type=\"hidden\" name={name} value={exported} />}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/signature-pad"
}
