{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "type": "registry:ui",
  "title": "Signature Pad",
  "description": "Smooth, speed-weighted ink with undo, a typed alternative and trimmed PNG/SVG export.",
  "categories": [
    "forms"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/signature-pad.tsx",
      "content": "\"use client\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease } from \"@/lib/motion\";\n\n/* ------------------------------------------------------------------ */\n/* Strokes                                                             */\n/* ------------------------------------------------------------------ */\n\n// Points are stored as fractions of the pad's width, so a signature survives resizes and rotation.\ntype Pt = { x: number; y: number; w: number };\ntype Stroke = Pt[];\n\nexport type Signature = {\n  kind: \"drawn\" | \"typed\";\n  /** The typed name, for kind \"typed\". */\n  name?: string;\n  /** A trimmed PNG with a transparent background. Ink defaults to near-black because signatures land on white documents. */\n  toPNG: (options?: { color?: string; scale?: number }) => Promise<Blob>;\n  /** A trimmed SVG string. */\n  toSVG: (options?: { color?: string }) => string;\n};\n\nconst mid = (a: Pt, b: Pt): Pt => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, w: (a.w + b.w) / 2 });\nconst EXPORT_INK = \"rgb(21 21 21)\";\n\n/**\n * Draws with quadratic curves through the midpoints of the samples, which turns\n * jagged pointer input into smooth ink. Each segment takes the width of its\n * control point, so speed shows as thinner line and hesitation as thicker.\n */\nfunction paint(ctx: CanvasRenderingContext2D, stroke: Stroke, scale: number, from = 0) {\n  const p = stroke;\n  const X = (v: number) => v * scale;\n  ctx.lineCap = \"round\";\n  ctx.lineJoin = \"round\";\n  if (p.length === 1) {\n    ctx.beginPath();\n    ctx.arc(X(p[0].x), X(p[0].y), Math.max(0.6, (p[0].w * scale) / 1.6), 0, Math.PI * 2);\n    ctx.fill();\n    return;\n  }\n  for (let i = Math.max(1, from); i < p.length; i++) {\n    const a = i === 1 ? p[0] : mid(p[i - 2], p[i - 1]);\n    const c = p[i - 1];\n    const b = mid(p[i - 1], p[i]);\n    ctx.beginPath();\n    ctx.lineWidth = c.w * scale;\n    ctx.moveTo(X(a.x), X(a.y));\n    ctx.quadraticCurveTo(X(c.x), X(c.y), X(b.x), X(b.y));\n    ctx.stroke();\n  }\n}\n\nfunction finish(ctx: CanvasRenderingContext2D, stroke: Stroke, scale: number) {\n  if (stroke.length < 2) return;\n  const a = mid(stroke[stroke.length - 2], stroke[stroke.length - 1]);\n  const b = stroke[stroke.length - 1];\n  ctx.beginPath();\n  ctx.lineWidth = b.w * scale;\n  ctx.moveTo(a.x * scale, a.y * scale);\n  ctx.lineTo(b.x * scale, b.y * scale);\n  ctx.stroke();\n}\n\nfunction bounds(strokes: Stroke[]) {\n  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n  for (const s of strokes)\n    for (const p of s) {\n      x0 = Math.min(x0, p.x - p.w);\n      y0 = Math.min(y0, p.y - p.w);\n      x1 = Math.max(x1, p.x + p.w);\n      y1 = Math.max(y1, p.y + p.w);\n    }\n  return { x0, y0, x1, y1 };\n}\n\nfunction drawnSignature(strokes: Stroke[], width: number): Signature {\n  const b = bounds(strokes);\n  const pad = 8 / width;\n  const box = { x: b.x0 - pad, y: b.y0 - pad, w: b.x1 - b.x0 + pad * 2, h: b.y1 - b.y0 + pad * 2 };\n  return {\n    kind: \"drawn\",\n    toSVG({ color = EXPORT_INK } = {}) {\n      const W = Math.round(box.w * width);\n      const H = Math.round(box.h * width);\n      const f = (v: number) => Math.round(v * width * 10) / 10;\n      const parts: string[] = [];\n      for (const s of strokes) {\n        const t = s.map((p) => ({ x: p.x - box.x, y: p.y - box.y, w: p.w }));\n        if (t.length === 1) {\n          parts.push(`<circle cx=\"${f(t[0].x)}\" cy=\"${f(t[0].y)}\" r=\"${f(t[0].w / 1.6)}\" fill=\"${color}\"/>`);\n          continue;\n        }\n        for (let i = 1; i < t.length; i++) {\n          const a = i === 1 ? t[0] : mid(t[i - 2], t[i - 1]);\n          const c = t[i - 1];\n          const e = mid(t[i - 1], t[i]);\n          parts.push(`<path d=\"M${f(a.x)} ${f(a.y)}Q${f(c.x)} ${f(c.y)} ${f(e.x)} ${f(e.y)}\" stroke-width=\"${f(c.w)}\"/>`);\n        }\n        const last = t[t.length - 1];\n        const m = mid(t[t.length - 2], last);\n        parts.push(`<path d=\"M${f(m.x)} ${f(m.y)}L${f(last.x)} ${f(last.y)}\" stroke-width=\"${f(last.w)}\"/>`);\n      }\n      return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${W} ${H}\" width=\"${W}\" height=\"${H}\"><g fill=\"none\" stroke=\"${color}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${parts.join(\"\")}</g></svg>`;\n    },\n    toPNG({ color = EXPORT_INK, scale = 2 } = {}) {\n      const c = document.createElement(\"canvas\");\n      c.width = Math.ceil(box.w * width * scale);\n      c.height = Math.ceil(box.h * width * scale);\n      const ctx = c.getContext(\"2d\")!;\n      ctx.strokeStyle = color;\n      ctx.fillStyle = color;\n      ctx.translate(-box.x * width * scale, -box.y * width * scale);\n      for (const s of strokes) {\n        paint(ctx, s, width * scale);\n        finish(ctx, s, width * scale);\n      }\n      return new Promise((resolve, reject) => c.toBlob((blob) => (blob ? resolve(blob) : reject(new Error(\"Export failed\"))), \"image/png\"));\n    },\n  };\n}\n\nfunction typedSignature(name: string, font: string): Signature {\n  const size = 44;\n  const measure = () => {\n    const ctx = document.createElement(\"canvas\").getContext(\"2d\")!;\n    ctx.font = `italic 500 ${size}px ${font}`;\n    return Math.ceil(ctx.measureText(name).width) + 24;\n  };\n  return {\n    kind: \"typed\",\n    name,\n    toSVG({ color = EXPORT_INK } = {}) {\n      const W = measure();\n      const esc = name.replace(/[&<>\"]/g, (ch) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[ch]!);\n      return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${W} 72\" width=\"${W}\" height=\"72\"><text x=\"12\" y=\"50\" font-family=\"${font.replace(/\"/g, \"'\")}\" font-size=\"${size}\" font-style=\"italic\" font-weight=\"500\" letter-spacing=\"-0.02em\" fill=\"${color}\">${esc}</text></svg>`;\n    },\n    toPNG({ color = EXPORT_INK, scale = 2 } = {}) {\n      const W = measure();\n      const c = document.createElement(\"canvas\");\n      c.width = W * scale;\n      c.height = 72 * scale;\n      const ctx = c.getContext(\"2d\")!;\n      ctx.scale(scale, scale);\n      ctx.font = `italic 500 ${size}px ${font}`;\n      ctx.fillStyle = color;\n      ctx.fillText(name, 12, 50);\n      return new Promise((resolve, reject) => c.toBlob((blob) => (blob ? resolve(blob) : reject(new Error(\"Export failed\"))), \"image/png\"));\n    },\n  };\n}\n\n// Mac check that agrees between server and first client render.\nconst noop = () => () => {};\nconst useIsMac = () => useSyncExternalStore(noop, () => /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent), () => true);\n\n/* ------------------------------------------------------------------ */\n/* The component                                                       */\n/* ------------------------------------------------------------------ */\n\nexport type SignaturePadProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  /** Called with the current signature after every stroke, undo, clear or edit to the typed name; null when empty. */\n  onSignatureChange?: (signature: Signature | null) => void;\n  /** Accessible name and the label over the pad. */\n  label?: string;\n  /** Offer typing a name instead of drawing. */\n  allowTyping?: boolean;\n  /** Prefills the typed name, e.g. from the account. */\n  defaultName?: string;\n  defaultMode?: \"draw\" | \"type\";\n  /** Pad height in pixels. */\n  height?: number;\n  disabled?: boolean;\n  /** A message from the form, such as \"Sign to continue\". */\n  error?: React.ReactNode;\n};\n\nexport function SignaturePad({\n  onSignatureChange,\n  label = \"Signature\",\n  allowTyping = true,\n  defaultName = \"\",\n  defaultMode = \"draw\",\n  height = 168,\n  disabled = false,\n  error,\n  className,\n  ...rest\n}: SignaturePadProps) {\n  const reduce = useReducedMotion();\n  const mac = useIsMac();\n  const id = useId();\n  const [mode, setMode] = useState<\"draw\" | \"type\">(allowTyping ? defaultMode : \"draw\");\n  const [strokes, setStrokes] = useState<Stroke[]>([]);\n  // Undo covers clearing too: a clear is pushed as the strokes it removed.\n  const [history, setHistory] = useState<Array<{ kind: \"stroke\" } | { kind: \"clear\"; strokes: Stroke[] }>>([]);\n  const [name, setName] = useState(defaultName);\n  const [started, setStarted] = useState(false);\n  const [clearing, setClearing] = useState(false);\n  const [said, setSaid] = useState(\"\");\n\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const nameRef = useRef<HTMLSpanElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const live = useRef<{ stroke: Stroke; last: { x: number; y: number; t: number }; id: number } | null>(null);\n  const widthRef = useRef(0);\n  const strokesRef = useRef<Stroke[]>([]);\n  const onChangeRef = useRef(onSignatureChange);\n  useEffect(() => {\n    onChangeRef.current = onSignatureChange;\n  });\n\n  const inkColor = () => (canvasRef.current ? getComputedStyle(canvasRef.current).color : EXPORT_INK);\n\n  // Full redraw at the current size and theme. Cheap: a signature is a few hundred segments.\n  const redraw = useCallback((list: Stroke[]) => {\n    const canvas = canvasRef.current;\n    const wrap = wrapRef.current;\n    if (!canvas || !wrap) return;\n    const w = wrap.clientWidth;\n    const h = wrap.clientHeight;\n    if (!w || !h) return;\n    const dpr = window.devicePixelRatio || 1;\n    if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n    }\n    widthRef.current = w;\n    const ctx = canvas.getContext(\"2d\")!;\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    ctx.clearRect(0, 0, w, h);\n    ctx.strokeStyle = ctx.fillStyle = getComputedStyle(canvas).color;\n    for (const s of list) {\n      paint(ctx, s, w);\n      finish(ctx, s, w);\n    }\n  }, []);\n\n  useEffect(() => {\n    strokesRef.current = strokes;\n    redraw(strokes);\n  }, [strokes, redraw]);\n\n  // Resizes and theme switches redraw from the stored points.\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    if (!wrap) return;\n    const ro = new ResizeObserver(() => redraw(strokesRef.current));\n    ro.observe(wrap);\n    const mo = new MutationObserver(() => redraw(strokesRef.current));\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] });\n    const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    const onScheme = () => redraw(strokesRef.current);\n    mq.addEventListener(\"change\", onScheme);\n    return () => {\n      ro.disconnect();\n      mo.disconnect();\n      mq.removeEventListener(\"change\", onScheme);\n    };\n  }, [redraw]);\n\n  const emit = useCallback(\n    (nextMode: \"draw\" | \"type\", list: Stroke[], typed: string) => {\n      const cb = onChangeRef.current;\n      if (!cb) return;\n      if (nextMode === \"type\") {\n        const font = nameRef.current ? getComputedStyle(nameRef.current).fontFamily : \"sans-serif\";\n        cb(typed.trim() ? typedSignature(typed.trim(), font) : null);\n      } else cb(list.length ? drawnSignature(list, widthRef.current || 1) : null);\n    },\n    [],\n  );\n\n  const commitStrokes = (next: Stroke[], note?: string) => {\n    setStrokes(next);\n    emit(\"draw\", next, name);\n    if (note) setSaid(note);\n  };\n\n  const undo = () => {\n    const last = history[history.length - 1];\n    if (!last) return;\n    setHistory((h) => h.slice(0, -1));\n    if (last.kind === \"clear\") commitStrokes(last.strokes, \"Signature restored\");\n    else {\n      if (strokes.length === 1) setStarted(false);\n      commitStrokes(strokes.slice(0, -1), strokes.length === 1 ? \"Signature empty\" : \"Stroke undone\");\n    }\n  };\n\n  const clear = () => {\n    if (!strokes.length) return;\n    setHistory((h) => [...h, { kind: \"clear\", strokes }]);\n    // The ink fades before it goes, so the clear is seen rather than a blink.\n    if (reduce) {\n      commitStrokes([], \"Signature cleared\");\n      setStarted(false);\n      return;\n    }\n    setClearing(true);\n    window.setTimeout(() => {\n      commitStrokes([], \"Signature cleared\");\n      setStarted(false);\n      setClearing(false);\n    }, 170);\n  };\n\n  const point = (e: React.PointerEvent | PointerEvent, prev?: { x: number; y: number; t: number; w: number }) => {\n    const r = canvasRef.current!.getBoundingClientRect();\n    const W = r.width;\n    const x = (e.clientX - r.left) / W;\n    const y = (e.clientY - r.top) / W;\n    const t = e.timeStamp;\n    const min = 1.1 / W;\n    const max = 3.4 / W;\n    let target: number;\n    if (e.pointerType === \"pen\" && e.pressure > 0) target = min + (max - min) * e.pressure;\n    else if (!prev) target = (min + max) / 2;\n    else {\n      // Faster strokes draw thinner, the way a nib lifts at speed.\n      const v = (Math.hypot(x - prev.x, y - prev.y) * W) / Math.max(1, t - prev.t);\n      target = Math.min(max, Math.max(min, max - v * 1.1 * (max - min)));\n    }\n    const w = prev ? prev.w * 0.72 + target * 0.28 : target;\n    return { x, y, t, w };\n  };\n\n  const empty = mode === \"draw\" ? strokes.length === 0 : !name.trim();\n  const hintId = `${id}-hint`;\n  const errorId = `${id}-error`;\n\n  return (\n    <div\n      data-slot=\"signature-pad\"\n      data-mode={mode}\n      data-empty={empty || undefined}\n      data-invalid={error ? true : undefined}\n      data-disabled={disabled || undefined}\n      className={cn(\"flex w-full min-w-0 flex-col gap-2\", disabled && \"pointer-events-none opacity-50\", className)}\n      {...rest}\n    >\n      <Tabs.Root\n        value={mode}\n        onValueChange={(v) => {\n          const next = v as \"draw\" | \"type\";\n          setMode(next);\n          emit(next, strokes, name);\n        }}\n        className=\"flex flex-col gap-2\"\n      >\n        <div className=\"flex min-h-7 items-center justify-between gap-3\">\n          <span id={`${id}-label`} className=\"text-[13px] font-medium tracking-[-0.01em] text-fg\">\n            {label}\n          </span>\n          {allowTyping && (\n            <Tabs.List aria-label=\"Signature method\" className=\"relative flex rounded-lg bg-hover p-0.5\">\n              <Tabs.Indicator\n                className={cn(\n                  \"absolute left-0 top-1/2 h-6 w-(--active-tab-width) -translate-y-1/2 translate-x-(--active-tab-left) rounded-md border border-line-2 bg-raised shadow-[var(--shadow)]\",\n                  \"transition-[translate,width] duration-200 ease-in-out-quart\",\n                )}\n              />\n              {([\"draw\", \"type\"] as const).map((m) => (\n                <Tabs.Tab\n                  key={m}\n                  value={m}\n                  className={cn(\n                    \"relative z-1 inline-flex h-6 items-center gap-1.5 rounded-md px-2 text-[12px] font-medium text-fg-3\",\n                    \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                    \"transition-[color,scale] duration-150 hover:text-fg-2 active:scale-[0.97] data-active:text-fg\",\n                  )}\n                >\n                  {m === \"draw\" ? <PenGlyph /> : <KeyboardGlyph />}\n                  {m === \"draw\" ? \"Draw\" : \"Type\"}\n                </Tabs.Tab>\n              ))}\n            </Tabs.List>\n          )}\n        </div>\n\n        <div\n          className={cn(\n            \"relative overflow-hidden rounded-xl border bg-raised shadow-[var(--shadow)] transition-[border-color,box-shadow] duration-150\",\n            error\n              ? \"border-danger/60 has-[canvas:focus-visible]:border-danger has-[canvas:focus-visible]:ring-3 has-[canvas:focus-visible]:ring-danger/15\"\n              : \"border-line-2 has-[canvas:focus-visible]:border-fg-3 has-[canvas:focus-visible]:ring-3 has-[canvas:focus-visible]:ring-fg/8\",\n          )}\n          style={{ height }}\n        >\n          <Tabs.Panel value=\"draw\" keepMounted className=\"group/draw absolute inset-0 outline-none\">\n            <div ref={wrapRef} className=\"absolute inset-0\">\n              <canvas\n                ref={canvasRef}\n                tabIndex={mode === \"draw\" ? 0 : -1}\n                role=\"img\"\n                aria-label={`${label}. ${strokes.length ? \"Signed.\" : \"Empty.\"}`}\n                aria-describedby={[hintId, error ? errorId : \"\"].filter(Boolean).join(\" \")}\n                data-clearing={clearing || undefined}\n                onPointerDown={(e) => {\n                  if (disabled || (e.pointerType === \"mouse\" && e.button !== 0)) return;\n                  e.currentTarget.setPointerCapture(e.pointerId);\n                  const p = point(e);\n                  live.current = { stroke: [{ x: p.x, y: p.y, w: p.w }], last: p, id: e.pointerId };\n                  setStarted(true);\n                  const ctx = e.currentTarget.getContext(\"2d\")!;\n                  ctx.strokeStyle = ctx.fillStyle = inkColor();\n                }}\n                onPointerMove={(e) => {\n                  const cur = live.current;\n                  if (!cur || cur.id !== e.pointerId) return;\n                  const ctx = e.currentTarget.getContext(\"2d\")!;\n                  // Coalesced events keep fast strokes smooth on 120Hz screens and pens.\n                  const events = e.nativeEvent.getCoalescedEvents?.() ?? [e.nativeEvent];\n                  for (const ev of events.length ? events : [e.nativeEvent]) {\n                    const prev = { ...cur.last, w: cur.stroke[cur.stroke.length - 1].w };\n                    const p = point(ev, prev);\n                    if (Math.hypot(p.x - prev.x, p.y - prev.y) * widthRef.current < 1.2) continue;\n                    cur.stroke.push({ x: p.x, y: p.y, w: p.w });\n                    cur.last = p;\n                    paint(ctx, cur.stroke, widthRef.current, cur.stroke.length - 1);\n                  }\n                }}\n                onPointerUp={(e) => {\n                  const cur = live.current;\n                  if (!cur || cur.id !== e.pointerId) return;\n                  live.current = null;\n                  const next = [...strokes, cur.stroke];\n                  setHistory((h) => [...h, { kind: \"stroke\" }]);\n                  commitStrokes(next, strokes.length === 0 ? \"Signature added\" : \"\");\n                }}\n                onPointerCancel={() => {\n                  live.current = null;\n                  redraw(strokes);\n                }}\n                onKeyDown={(e) => {\n                  if (e.key.toLowerCase() === \"z\" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {\n                    e.preventDefault();\n                    undo();\n                  } else if (e.key === \"Delete\" || e.key === \"Backspace\") {\n                    e.preventDefault();\n                    clear();\n                  } else if (allowTyping && e.key.toLowerCase() === \"t\" && !e.metaKey && !e.ctrlKey) {\n                    e.preventDefault();\n                    setMode(\"type\");\n                    emit(\"type\", strokes, name);\n                    requestAnimationFrame(() => inputRef.current?.focus());\n                  }\n                }}\n                className={cn(\n                  \"absolute inset-0 size-full cursor-crosshair touch-none text-fg outline-none select-none\",\n                  \"transition-opacity duration-150 ease-out data-clearing:opacity-0\",\n                )}\n              />\n            </div>\n\n            {/* The baseline and the × stay as a guide; the words fade once the pen touches down. */}\n            <div aria-hidden className=\"pointer-events-none absolute inset-x-5 bottom-[30%] flex items-end gap-2\">\n              <span className=\"pb-0.5 text-[13px] leading-none text-fg-4\">×</span>\n              <span className=\"h-px flex-1 bg-line-2\" />\n            </div>\n            <span\n              aria-hidden\n              className={cn(\n                \"pointer-events-none absolute bottom-[30%] left-10 mb-2 text-[12px] text-fg-4 transition-[opacity,translate] duration-200 ease-out\",\n                started || strokes.length ? \"translate-y-1 opacity-0 duration-150\" : \"opacity-100\",\n              )}\n            >\n              Sign here\n            </span>\n            <span\n              id={hintId}\n              className={cn(\n                \"pointer-events-none absolute bottom-2.5 left-5 right-5 text-[11px] text-fg-4 opacity-0 transition-opacity duration-150\",\n                // Keyboard users land here and learn the alternatives; pointer users never see it.\n                \"group-has-[canvas:focus-visible]/draw:opacity-100\",\n              )}\n            >\n              Draw with a mouse, finger or pen.{allowTyping ? \" Press T to type your name instead.\" : \"\"} {mac ? \"⌘Z\" : \"Ctrl+Z\"} undoes.\n            </span>\n          </Tabs.Panel>\n\n          {allowTyping && (\n            <Tabs.Panel value=\"type\" keepMounted className=\"absolute inset-0 flex flex-col justify-end outline-none\">\n              <label htmlFor={`${id}-name`} className=\"sr-only\">\n                Type your full name\n              </label>\n              {/* The typed name is set large on the same baseline as the drawn one. */}\n              <div className=\"pointer-events-none absolute inset-x-5 bottom-[30%] flex items-end gap-2\">\n                <span aria-hidden className=\"pb-0.5 text-[13px] leading-none text-fg-4\">×</span>\n                <span className=\"relative min-w-0 flex-1\">\n                  <span\n                    ref={nameRef}\n                    aria-hidden\n                    className={cn(\n                      \"block truncate pb-1 leading-none font-medium italic tracking-[-0.02em] text-fg\",\n                      // Long names step down a size before they would truncate.\n                      name.length > 26 ? \"text-[22px]\" : name.length > 18 ? \"text-[28px]\" : \"text-[34px]\",\n                    )}\n                  >\n                    {name || \" \"}\n                  </span>\n                  <span aria-hidden className=\"block h-px bg-line-2\" />\n                </span>\n              </div>\n              <div className=\"relative m-2 flex h-9 items-center rounded-lg border border-line-2 bg-frame px-3 transition-[border-color,box-shadow] duration-150 focus-within:border-fg-3 focus-within:ring-3 focus-within:ring-fg/8\">\n                <input\n                  ref={inputRef}\n                  id={`${id}-name`}\n                  value={name}\n                  tabIndex={mode === \"type\" ? 0 : -1}\n                  onChange={(e) => {\n                    setName(e.currentTarget.value);\n                    emit(\"type\", strokes, e.currentTarget.value);\n                  }}\n                  placeholder=\"Type your full name\"\n                  autoComplete=\"name\"\n                  autoCapitalize=\"words\"\n                  spellCheck={false}\n                  enterKeyHint=\"done\"\n                  maxLength={80}\n                  aria-describedby={error ? errorId : undefined}\n                  className=\"w-full min-w-0 bg-transparent text-base text-fg outline-none placeholder:text-fg-4 sm:text-[13px]\"\n                />\n              </div>\n            </Tabs.Panel>\n          )}\n        </div>\n      </Tabs.Root>\n\n      <div className=\"flex min-h-7 items-center justify-between gap-3\">\n        <div className=\"grid min-w-0 text-[12px] leading-[18px]\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={error ? \"error\" : mode === \"type\" ? \"type\" : \"draw\"}\n              id={error ? errorId : undefined}\n              className={cn(\"col-start-1 row-start-1 min-w-0 text-pretty\", error ? \"text-danger\" : \"text-fg-3\")}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 3 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, transition: { duration: 0.1 } }}\n              transition={{ duration: 0.2, ease: ease.out }}\n            >\n              {error ?? (mode === \"type\" ? \"Typing your name is as binding as drawing it.\" : \"Your signature stays on this device until you submit.\")}\n            </motion.span>\n          </AnimatePresence>\n        </div>\n        {mode === \"draw\" && (\n          <div className=\"flex shrink-0 items-center gap-0.5\">\n            <IconButton label=\"Undo\" shortcut={mac ? \"⌘Z\" : \"Ctrl+Z\"} disabled={!history.length} onClick={undo}>\n              <path d=\"M5.5 5H10a3.25 3.25 0 0 1 0 6.5H6M5.5 5 7.75 2.75M5.5 5l2.25 2.25\" />\n            </IconButton>\n            <button\n              type=\"button\"\n              disabled={!strokes.length}\n              onClick={clear}\n              className={cn(\n                \"relative inline-flex h-7 items-center rounded-md px-2 text-[12px] font-medium text-fg-2\",\n                \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n                \"transition-[background-color,color,scale,opacity] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.97] disabled:pointer-events-none disabled:opacity-40\",\n                \"before:absolute before:-inset-y-2 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n              )}\n            >\n              Clear\n            </button>\n          </div>\n        )}\n      </div>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {said}\n      </span>\n    </div>\n  );\n}\n\nfunction IconButton({ label, shortcut, children, ...rest }: React.ComponentProps<\"button\"> & { label: string; shortcut?: string }) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      aria-keyshortcuts={shortcut === \"⌘Z\" ? \"Meta+Z\" : shortcut ? \"Control+Z\" : undefined}\n      title={shortcut ? `${label} (${shortcut})` : label}\n      className={cn(\n        \"relative grid size-7 place-items-center rounded-md text-fg-2\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-fg-3\",\n        \"transition-[background-color,color,scale,opacity] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.9] disabled:pointer-events-none disabled:opacity-40\",\n        \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n      )}\n      {...rest}\n    >\n      <svg aria-hidden width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n        {children}\n      </svg>\n    </button>\n  );\n}\n\nfunction PenGlyph() {\n  return (\n    <svg aria-hidden width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <path d=\"M10.5 3.25a1.4 1.4 0 0 1 2 2L5.75 12 3 12.75 3.75 10z\" />\n    </svg>\n  );\n}\n\nfunction KeyboardGlyph() {\n  return (\n    <svg aria-hidden width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <rect x=\"1.75\" y=\"4\" width=\"12.5\" height=\"8\" rx=\"1.5\" />\n      <path d=\"M4.5 6.75h.01M7 6.75h.01M9.5 6.75h.01M12 6.75h-.5M5.5 9.25h5\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/signature-pad"
}
