{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"signature-pad","type":"registry:ui","title":"Shadcn signature pad with pressure and velocity ink, touch and stylus input, undo and redo, form fields and PNG, JPEG or SVG export.","description":"Shadcn signature pad with pressure and velocity ink, touch and stylus input, undo and redo, form fields and PNG, JPEG or SVG export.","dependencies":["cn"],"registryDependencies":["button"],"files":[{"path":"signature-pad.tsx","type":"registry:ui","content":"\"use client\"\n\n// Title: Signature Pad\n// Description: Shadcn signature pad with pressure and velocity ink, touch and stylus input, undo and redo, form fields and PNG, JPEG or SVG export.\nimport {\n  createContext,\n  memo,\n  useContext,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\"\nimport type {\n  ComponentProps,\n  KeyboardEvent as ReactKeyboardEvent,\n  PointerEvent as ReactPointerEvent,\n  Ref,\n} from \"react\"\nimport { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\n/* -------------------------------------------------------------------------- */\n/*                                    Types                                   */\n/* -------------------------------------------------------------------------- */\n\n/** CSS pixels from the area's top-left corner, and the ink diameter there. */\nexport type SignaturePadPoint = [x: number, y: number, size: number]\n\nexport type SignaturePadStroke = {\n  points: SignaturePadPoint[]\n  /** Recorded only when the pad has a `color`; otherwise the ink follows `currentColor`. */\n  color?: string\n}\n\nexport type SignaturePadPointerType = \"mouse\" | \"pen\" | \"touch\"\n\n/**\n * `auto` reads pen pressure and falls back to speed for mouse and touch,\n * whose `pressure` is a constant 0.5 on most hardware.\n */\nexport type SignaturePadSizing = \"auto\" | \"pressure\" | \"velocity\"\n\n/** Data URLs for the image formats, the raw strokes for `json`. */\nexport type SignaturePadFormat = \"png\" | \"jpeg\" | \"svg\" | \"json\"\n\nexport type SignaturePadExportOptions = {\n  /** A fixed frame from the area's origin. Without both, the image is cropped to the ink. */\n  width?: number\n  height?: number\n  /** Space kept around the ink when cropping. */\n  padding?: number\n  /** `false` keeps the area's origin instead of cropping to the ink. */\n  crop?: boolean\n  /** Ink for strokes that carry no color of their own. Defaults to black, since exports usually land on paper. */\n  color?: string\n  background?: string\n  /** Raster only: pixels per CSS pixel. */\n  scale?: number\n  /** Raster only. */\n  type?: \"image/png\" | \"image/jpeg\" | \"image/webp\"\n  quality?: number\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Geometry                                  */\n/* -------------------------------------------------------------------------- */\n\nconst round = (value: number) => Math.round(value * 100) / 100\nconst pair = (x: number, y: number) => `${round(x)} ${round(y)}`\n\n/* Past ~100 degrees the averaged tangent flips and the outline pinches, so a\n   stroke is split there and each piece gets its own round caps. */\nconst CORNER_COS = -0.17\n\nfunction circlePath(x: number, y: number, r: number) {\n  const radius = round(Math.max(r, 0.25))\n  return `M${pair(x - radius, y)}a${radius} ${radius} 0 1 0 ${round(radius * 2)} 0a${radius} ${radius} 0 1 0 ${round(-radius * 2)} 0Z`\n}\n\n/**\n * One filled outline: the left offset forward, a round cap, the right offset\n * back, a round cap. Every arc sweeps the same way as the outline itself, so\n * overlapping pieces and self-crossing loops add up under the default\n * `nonzero` fill instead of punching holes.\n */\nfunction outlinePath(points: SignaturePadPoint[], from: number, to: number) {\n  const [startX, startY, startSize] = points[from]\n  if (from === to) return circlePath(startX, startY, startSize / 2)\n\n  const left: [number, number][] = []\n  const right: [number, number][] = []\n  let nx = 0\n  let ny = 1\n\n  for (let i = from; i <= to; i++) {\n    const [ax, ay] = points[Math.max(i - 1, from)]\n    const [bx, by] = points[Math.min(i + 1, to)]\n    const length = Math.hypot(bx - ax, by - ay)\n    if (length > 1e-6) {\n      nx = -(by - ay) / length\n      ny = (bx - ax) / length\n    }\n    const [x, y, size] = points[i]\n    const r = size / 2\n    left.push([x + nx * r, y + ny * r])\n    right.push([x - nx * r, y - ny * r])\n  }\n\n  const last = left.length - 1\n  const endRadius = round(points[to][2] / 2)\n  const startRadius = round(startSize / 2)\n\n  let d = `M${pair(...left[0])}`\n  for (let i = 1; i < last; i++) {\n    const [x, y] = left[i]\n    d += `Q${pair(x, y)} ${pair((x + left[i + 1][0]) / 2, (y + left[i + 1][1]) / 2)}`\n  }\n  d += `L${pair(...left[last])}A${endRadius} ${endRadius} 0 0 0 ${pair(...right[last])}`\n  for (let i = last - 1; i > 0; i--) {\n    const [x, y] = right[i]\n    d += `Q${pair(x, y)} ${pair((x + right[i - 1][0]) / 2, (y + right[i - 1][1]) / 2)}`\n  }\n  return `${d}L${pair(...right[0])}A${startRadius} ${startRadius} 0 0 0 ${pair(...left[0])}Z`\n}\n\nfunction isCorner(\n  a: SignaturePadPoint,\n  b: SignaturePadPoint,\n  c: SignaturePadPoint\n) {\n  const ux = b[0] - a[0]\n  const uy = b[1] - a[1]\n  const vx = c[0] - b[0]\n  const vy = c[1] - b[1]\n  const lengths = Math.hypot(ux, uy) * Math.hypot(vx, vy)\n  return lengths > 0 && (ux * vx + uy * vy) / lengths < CORNER_COS\n}\n\n/** The SVG path data of one stroke's filled outline, shared by the pad and every export. */\nfunction getSignaturePadStrokePath(stroke: SignaturePadStroke) {\n  const { points } = stroke\n  if (points.length === 0) return \"\"\n\n  let d = \"\"\n  let start = 0\n  for (let i = 1; i < points.length - 1; i++) {\n    if (isCorner(points[i - 1], points[i], points[i + 1])) {\n      d += outlinePath(points, start, i)\n      start = i\n    }\n  }\n  return d + outlinePath(points, start, points.length - 1)\n}\n\n/** The ink's bounding box, stroke width included, or `null` when there is no ink. */\nfunction getSignaturePadBounds(strokes: SignaturePadStroke[]) {\n  let minX = Infinity\n  let minY = Infinity\n  let maxX = -Infinity\n  let maxY = -Infinity\n\n  for (const stroke of strokes) {\n    for (const [x, y, size] of stroke.points) {\n      const r = size / 2\n      minX = Math.min(minX, x - r)\n      minY = Math.min(minY, y - r)\n      maxX = Math.max(maxX, x + r)\n      maxY = Math.max(maxY, y + r)\n    }\n  }\n\n  if (minX === Infinity) return null\n  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Export                                   */\n/* -------------------------------------------------------------------------- */\n\nfunction resolveFrame(\n  strokes: SignaturePadStroke[],\n  options: SignaturePadExportOptions\n) {\n  const { width, height, padding = 8, crop = true } = options\n  if (width !== undefined && height !== undefined) {\n    return { x: 0, y: 0, width, height }\n  }\n\n  const bounds = getSignaturePadBounds(strokes)\n  if (!bounds) return { x: 0, y: 0, width: width ?? 1, height: height ?? 1 }\n  if (!crop) {\n    return {\n      x: 0,\n      y: 0,\n      width: width ?? Math.ceil(bounds.x + bounds.width + padding),\n      height: height ?? Math.ceil(bounds.y + bounds.height + padding),\n    }\n  }\n  /* Round the edges, not the size: flooring x and ceiling only the width\n     could leave the right and bottom edges up to 1px inside the ink. */\n  const x = Math.floor(bounds.x - padding)\n  const y = Math.floor(bounds.y - padding)\n  return {\n    x,\n    y,\n    width: Math.ceil(bounds.x + bounds.width + padding) - x,\n    height: Math.ceil(bounds.y + bounds.height + padding) - y,\n  }\n}\n\nconst escapeAttribute = (value: string) =>\n  value.replace(/[&\"<>]/g, (char) => `&#${char.charCodeAt(0)};`)\n\n/** A standalone SVG document string. */\nfunction signaturePadToSVG(\n  strokes: SignaturePadStroke[],\n  options: SignaturePadExportOptions = {}\n) {\n  const { x, y, width, height } = resolveFrame(strokes, options)\n  const color = options.color ?? \"#000000\"\n  const background = options.background\n    ? `<rect x=\"${x}\" y=\"${y}\" width=\"${width}\" height=\"${height}\" fill=\"${escapeAttribute(options.background)}\"/>`\n    : \"\"\n  const paths = strokes\n    .map((stroke) => {\n      const d = getSignaturePadStrokePath(stroke)\n      return d\n        ? `<path d=\"${d}\" fill=\"${escapeAttribute(stroke.color ?? color)}\"/>`\n        : \"\"\n    })\n    .join(\"\")\n\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${x} ${y} ${width} ${height}\" width=\"${width}\" height=\"${height}\">${background}${paths}</svg>`\n}\n\nfunction drawToCanvas(\n  strokes: SignaturePadStroke[],\n  options: SignaturePadExportOptions\n) {\n  const { x, y, width, height } = resolveFrame(strokes, options)\n  const scale = options.scale ?? 2\n  const canvas = document.createElement(\"canvas\")\n  canvas.width = Math.max(1, Math.round(width * scale))\n  canvas.height = Math.max(1, Math.round(height * scale))\n\n  const context = canvas.getContext(\"2d\")\n  if (!context) return canvas\n  context.scale(scale, scale)\n  context.translate(-x, -y)\n\n  /* JPEG has no alpha, so transparent ink would land on black. */\n  const background =\n    options.background ?? (options.type === \"image/jpeg\" ? \"#ffffff\" : \"\")\n  if (background) {\n    context.fillStyle = background\n    context.fillRect(x, y, width, height)\n  }\n\n  for (const stroke of strokes) {\n    const d = getSignaturePadStrokePath(stroke)\n    if (!d) continue\n    context.fillStyle = stroke.color ?? options.color ?? \"#000000\"\n    context.fill(new Path2D(d))\n  }\n  return canvas\n}\n\n/** A PNG data URL, or `type`. Browser only: call it from a handler or an effect, never while rendering. */\nfunction signaturePadToDataURL(\n  strokes: SignaturePadStroke[],\n  options: SignaturePadExportOptions = {}\n) {\n  return drawToCanvas(strokes, options).toDataURL(\n    options.type ?? \"image/png\",\n    options.quality\n  )\n}\n\n/** For uploads: a `Blob` skips the base64 inflation of a data URL. Browser only. */\nfunction signaturePadToBlob(\n  strokes: SignaturePadStroke[],\n  options: SignaturePadExportOptions = {}\n) {\n  return new Promise<Blob | null>((resolve) => {\n    drawToCanvas(strokes, options).toBlob(\n      resolve,\n      options.type ?? \"image/png\",\n      options.quality\n    )\n  })\n}\n\n/** One string per format, and `\"\"` for an empty pad so a `required` field fails. `png` and `jpeg` need a browser. */\nfunction serializeSignaturePad(\n  strokes: SignaturePadStroke[],\n  format: SignaturePadFormat = \"svg\",\n  options: SignaturePadExportOptions = {}\n) {\n  if (strokes.length === 0) return \"\"\n  if (format === \"json\") return JSON.stringify(strokes)\n  if (format === \"svg\") {\n    return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(signaturePadToSVG(strokes, options))}`\n  }\n  return signaturePadToDataURL(strokes, {\n    ...options,\n    type: format === \"jpeg\" ? \"image/jpeg\" : \"image/png\",\n  })\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Context                                  */\n/* -------------------------------------------------------------------------- */\n\nexport type SignaturePadApi = {\n  strokes: SignaturePadStroke[]\n  isEmpty: boolean\n  isDrawing: boolean\n  canUndo: boolean\n  canRedo: boolean\n  disabled: boolean\n  readOnly: boolean\n  clear: () => void\n  undo: () => void\n  redo: () => void\n  /** Focuses the drawing area. */\n  focus: () => void\n  /** With `crop: false` and no size, these four frame the whole area as rendered. */\n  toSVG: (options?: SignaturePadExportOptions) => string\n  toDataURL: (options?: SignaturePadExportOptions) => string\n  toBlob: (options?: SignaturePadExportOptions) => Promise<Blob | null>\n  serialize: (\n    format?: SignaturePadFormat,\n    options?: SignaturePadExportOptions\n  ) => string\n}\n\nconst API_KEYS = [\n  \"strokes\",\n  \"isEmpty\",\n  \"isDrawing\",\n  \"canUndo\",\n  \"canRedo\",\n  \"disabled\",\n  \"readOnly\",\n  \"clear\",\n  \"undo\",\n  \"redo\",\n  \"focus\",\n  \"toSVG\",\n  \"toDataURL\",\n  \"toBlob\",\n  \"serialize\",\n] as const satisfies readonly (keyof SignaturePadApi)[]\n\ntype SignaturePadConfig = {\n  color?: string\n  minWidth: number\n  maxWidth: number\n  smoothing: number\n  sizing: SignaturePadSizing\n  pointerTypes?: SignaturePadPointerType[]\n  name?: string\n  form?: string\n  required: boolean\n  format: SignaturePadFormat\n  interactive: boolean\n  undoDepth: number\n  redoDepth: number\n  setArea: (node: HTMLDivElement | null) => void\n  setDrawing: (drawing: boolean) => void\n  commitStroke: (stroke: SignaturePadStroke) => void\n  resetToDefault: () => void\n  onStrokeStart?: (details: { pointerType: SignaturePadPointerType }) => void\n}\n\nconst SignaturePadContext = createContext<SignaturePadApi | null>(null)\nconst SignaturePadConfigContext = createContext<SignaturePadConfig | null>(null)\n\nfunction useSignaturePad(): SignaturePadApi {\n  const context = useContext(SignaturePadContext)\n  if (!context) {\n    throw new Error(\"useSignaturePad must be used within a SignaturePad\")\n  }\n  return context\n}\n\nfunction useSignaturePadConfig(part: string) {\n  const context = useContext(SignaturePadConfigContext)\n  if (!context) throw new Error(`${part} must be used within a SignaturePad`)\n  return context\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Root                                    */\n/* -------------------------------------------------------------------------- */\n\nconst HISTORY_LIMIT = 100\n\n/* Structural, not identity: react-hook-form and other stores hand back a\n   deep copy of the value, and identity alone read every copy as an outside\n   change and voided undo. First and last point are enough to tell a copy from\n   a real edit. */\nfunction sameStrokes(a: SignaturePadStroke[], b: SignaturePadStroke[]) {\n  if (a === b) return true\n  if (a.length !== b.length) return false\n  return a.every((stroke, index) => {\n    const other = b[index]\n    if (stroke === other) return true\n    const { points } = stroke\n    const last = points.length - 1\n    return (\n      stroke.color === other.color &&\n      points.length === other.points.length &&\n      (last < 0 ||\n        (points[0].join() === other.points[0].join() &&\n          points[last].join() === other.points[last].join()))\n    )\n  })\n}\n\n/* Next still server-renders \"use client\" files, and a bare useLayoutEffect\n   logs on every SSR pass. Same alias the code block ships. */\nconst useIsoLayoutEffect =\n  typeof window !== \"undefined\" ? useLayoutEffect : useEffect\n\ntype History = {\n  past: SignaturePadStroke[][]\n  future: SignaturePadStroke[][]\n  /** The value this history leads to. A value it does not match came from outside, and the history is void. */\n  present: SignaturePadStroke[]\n}\n\nexport type SignaturePadProps = Omit<\n  ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> & {\n  /** Strokes are compared by content, so a copy of the same value keeps undo; a different value from outside clears it. */\n  value?: SignaturePadStroke[]\n  defaultValue?: SignaturePadStroke[]\n  /** Fires once per finished stroke, and on clear, undo and redo. Never per pointer move. */\n  onValueChange?: (value: SignaturePadStroke[]) => void\n  onStrokeStart?: (details: { pointerType: SignaturePadPointerType }) => void\n  onStrokeEnd?: (stroke: SignaturePadStroke) => void\n  /** The pad's API, for code outside the tree (a form library, a toolbar elsewhere). */\n  apiRef?: Ref<SignaturePadApi>\n  /** Any CSS color. Unset, the ink follows the area's text color, so it tracks the theme. */\n  color?: string\n  /** Ink diameter range in CSS pixels. Equal values draw a constant line. */\n  minWidth?: number\n  maxWidth?: number\n  /** 0 is raw input; higher trades a little lag for smoother curves. */\n  smoothing?: number\n  sizing?: SignaturePadSizing\n  /** Accept only these pointers, e.g. `[\"pen\"]` to ignore a resting palm. */\n  pointerTypes?: SignaturePadPointerType[]\n  disabled?: boolean\n  readOnly?: boolean\n  /** Submits the signature with a native form under this name. */\n  name?: string\n  form?: string\n  required?: boolean\n  /** What the form field carries. `png` and `jpeg` rasterize on every stroke; `svg` is a string build. */\n  format?: SignaturePadFormat\n}\n\nfunction SignaturePad({\n  value,\n  defaultValue,\n  onValueChange,\n  onStrokeStart,\n  onStrokeEnd,\n  apiRef,\n  color,\n  minWidth = 0.8,\n  maxWidth = 3.2,\n  smoothing = 0.5,\n  sizing = \"auto\",\n  pointerTypes,\n  disabled = false,\n  readOnly = false,\n  name,\n  form,\n  required = false,\n  format = \"svg\",\n  className,\n  children,\n  ...props\n}: SignaturePadProps) {\n  const [uncontrolled, setUncontrolled] = useState<SignaturePadStroke[]>(\n    () => defaultValue ?? []\n  )\n  const strokes = value ?? uncontrolled\n  /* What a form reset restores, as a native field returns to its default. */\n  const [initialValue] = useState<SignaturePadStroke[]>(\n    () => defaultValue ?? []\n  )\n  const [history, setHistory] = useState<History>(() => ({\n    past: [],\n    future: [],\n    present: strokes,\n  }))\n  const [isDrawing, setDrawing] = useState(false)\n  const areaRef = useRef<HTMLDivElement | null>(null)\n\n  const synced = sameStrokes(history.present, strokes)\n  const past = synced ? history.past : []\n  const future = synced ? history.future : []\n\n  const setStrokes = (next: SignaturePadStroke[], nextHistory: History) => {\n    setHistory(nextHistory)\n    if (value === undefined) setUncontrolled(next)\n    onValueChange?.(next)\n  }\n\n  const commit = (next: SignaturePadStroke[]) =>\n    setStrokes(next, {\n      past: [...past, strokes].slice(-HISTORY_LIMIT),\n      future: [],\n      present: next,\n    })\n\n  /* With `crop: false` and no size, an export frames the area as rendered. */\n  const withAreaFrame = (options: SignaturePadExportOptions = {}) => {\n    const area = areaRef.current\n    if (options.crop !== false || !area) return options\n    return {\n      ...options,\n      width: options.width ?? area.clientWidth,\n      height: options.height ?? area.clientHeight,\n    }\n  }\n\n  const api: SignaturePadApi = {\n    strokes,\n    isEmpty: strokes.length === 0,\n    isDrawing,\n    canUndo: past.length > 0,\n    canRedo: future.length > 0,\n    disabled,\n    readOnly,\n    clear: () => {\n      if (strokes.length > 0) commit([])\n    },\n    undo: () => {\n      const previous = past[past.length - 1]\n      if (!previous) return\n      setStrokes(previous, {\n        past: past.slice(0, -1),\n        future: [strokes, ...future],\n        present: previous,\n      })\n    },\n    redo: () => {\n      const next = future[0]\n      if (!next) return\n      setStrokes(next, {\n        past: [...past, strokes],\n        future: future.slice(1),\n        present: next,\n      })\n    },\n    focus: () => areaRef.current?.focus({ preventScroll: true }),\n    toSVG: (options) => signaturePadToSVG(strokes, withAreaFrame(options)),\n    toDataURL: (options) =>\n      signaturePadToDataURL(strokes, withAreaFrame(options)),\n    toBlob: (options) => signaturePadToBlob(strokes, withAreaFrame(options)),\n    serialize: (format, options) =>\n      serializeSignaturePad(strokes, format, withAreaFrame(options)),\n  }\n\n  /* Members read the latest render through getters, and the object is\n     rebuilt only when the pad's state changes. A handle rebuilt on every\n     render made a state-setter `apiRef` loop until React threw, including the\n     inline `(h) => setApi(h)` form React re-attaches each render; a handle\n     that never changed left such a consumer frozen at its mount state. */\n  const latestApi = useRef<SignaturePadApi>(null)\n  const handleRef = useRef<{ key: unknown[]; handle: SignaturePadApi }>(null)\n  useIsoLayoutEffect(() => {\n    latestApi.current = api\n  })\n  useImperativeHandle(apiRef, () => {\n    const key = [strokes, history, isDrawing, disabled, readOnly]\n    const cached = handleRef.current\n    if (cached && cached.key.every((item, i) => Object.is(item, key[i]))) {\n      return cached.handle\n    }\n    const handle = {} as SignaturePadApi\n    for (const name of API_KEYS) {\n      Object.defineProperty(handle, name, {\n        enumerable: true,\n        get: () => latestApi.current![name],\n      })\n    }\n    handleRef.current = { key, handle }\n    return handle\n  }, [strokes, history, isDrawing, disabled, readOnly])\n\n  const config: SignaturePadConfig = {\n    color,\n    minWidth,\n    maxWidth: Math.max(minWidth, maxWidth),\n    smoothing: Math.min(Math.max(smoothing, 0), 1),\n    sizing,\n    pointerTypes,\n    name,\n    form,\n    required,\n    format,\n    interactive: !disabled && !readOnly,\n    undoDepth: past.length,\n    redoDepth: future.length,\n    setArea: (node) => {\n      areaRef.current = node\n    },\n    setDrawing,\n    commitStroke: (stroke) => {\n      commit([...strokes, stroke])\n      onStrokeEnd?.(stroke)\n    },\n    resetToDefault: () => {\n      if (!sameStrokes(strokes, initialValue)) commit(initialValue)\n    },\n    onStrokeStart,\n  }\n\n  return (\n    <SignaturePadContext.Provider value={api}>\n      <SignaturePadConfigContext.Provider value={config}>\n        <div\n          data-slot=\"signature-pad\"\n          data-empty={strokes.length === 0 || undefined}\n          data-drawing={isDrawing || undefined}\n          data-disabled={disabled || undefined}\n          data-readonly={readOnly || undefined}\n          className={cn(\"flex w-full flex-col gap-2\", className)}\n          {...props}\n        >\n          {children}\n        </div>\n      </SignaturePadConfigContext.Provider>\n    </SignaturePadContext.Provider>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Area                                    */\n/* -------------------------------------------------------------------------- */\n\nconst AREA_BASE_CLASS =\n  \"relative h-40 w-full overflow-hidden border text-foreground outline-none touch-none select-none [-webkit-touch-callout:none] cursor-crosshair data-disabled:cursor-not-allowed data-disabled:opacity-50 data-readonly:cursor-default\"\n\n/* Radius, focus and invalid rings, copied token for token from the textarea\n   ladder so the pad reads as the same kind of control in every style. Every\n   variant takes these. */\nconst AREA_STATE_CLASS =\n  \"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg transition-colors focus-visible:ring-3 aria-invalid:ring-3\"\n\n/* Kept apart from the rings because a style-prefixed fill outranks a plain\n   one: sharing a ladder, `muted` could never paint over the textarea's\n   `bg-transparent`. The `disabled:` tokens become `data-disabled:`, since a\n   div never matches `:disabled`. */\nconst AREA_VARIANT_CLASS = {\n  default:\n    \"border-input bg-transparent dark:bg-input/30 data-disabled:bg-input/50 dark:data-disabled:bg-input/80\",\n  muted: \"border-border bg-muted/50\",\n  ghost: \"border-transparent bg-transparent\",\n}\n\ntype LiveStroke = {\n  pointerId: number\n  pointerType: string\n  points: SignaturePadPoint[]\n  left: number\n  top: number\n  scaleX: number\n  scaleY: number\n  time: number\n  velocity: number\n  size: number\n  /* The unsmoothed last position. Streamline trails the pointer, so without\n     it a stroke lifted in motion ended short (12px on a fast 200px line). */\n  rawX: number\n  rawY: number\n}\n\nconst StrokePath = memo(function StrokePath({\n  stroke,\n}: {\n  stroke: SignaturePadStroke\n}) {\n  return (\n    <path\n      data-slot=\"signature-pad-stroke\"\n      d={getSignaturePadStrokePath(stroke)}\n      fill={stroke.color ?? \"currentColor\"}\n    />\n  )\n})\n\nexport type SignaturePadAreaProps = ComponentProps<\"div\"> & {\n  /** `muted` is a filled surface; `ghost` drops the chrome for a card or document that has its own. */\n  variant?: keyof typeof AREA_VARIANT_CLASS\n}\n\n/**\n * The drawing surface. Children are overlays (a guide, a placeholder, pinned\n * controls) and must be `pointer-events-none` unless they are controls: a\n * stroke only starts on the area itself, never on something inside it.\n */\nfunction SignaturePadArea({\n  ref,\n  variant = \"default\",\n  className,\n  children,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n  onPointerCancel,\n  onLostPointerCapture,\n  onKeyDown,\n  onContextMenu,\n  \"aria-describedby\": describedBy,\n  ...props\n}: SignaturePadAreaProps) {\n  const api = useSignaturePad()\n  const config = useSignaturePadConfig(\"SignaturePadArea\")\n  const liveRef = useRef<LiveStroke | null>(null)\n  const pathRef = useRef<SVGPathElement>(null)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const frameRef = useRef(0)\n  const resetRef = useRef(config.resetToDefault)\n\n  useEffect(() => {\n    resetRef.current = config.resetToDefault\n  })\n\n  /* Set imperatively: a PNG needs a canvas, which only exists in the browser,\n     and a controlled `value` would then differ between server and client. */\n  useEffect(() => {\n    if (inputRef.current) {\n      inputRef.current.value = serializeSignaturePad(api.strokes, config.format)\n    }\n  }, [api.strokes, config.format, config.name])\n\n  /* Listens on the input's own root (the document, or a shadow root) and asks\n     for its form at reset time, so a `form` attribute or an owner that\n     changes later is still honoured. The restore waits a task, like a native\n     reset's default action, because any listener may still cancel it. */\n  useEffect(() => {\n    const root = inputRef.current?.getRootNode()\n    if (!root) return\n    let pending = 0\n    const onReset = (event: Event) => {\n      if (event.target !== inputRef.current?.form) return\n      pending = window.setTimeout(() => {\n        if (!event.defaultPrevented) resetRef.current()\n      })\n    }\n    root.addEventListener(\"reset\", onReset, true)\n    return () => {\n      root.removeEventListener(\"reset\", onReset, true)\n      window.clearTimeout(pending)\n    }\n  }, [config.name])\n\n  useEffect(() => () => cancelAnimationFrame(frameRef.current), [])\n\n  const paint = () => {\n    frameRef.current = 0\n    const live = liveRef.current\n    pathRef.current?.setAttribute(\n      \"d\",\n      live ? getSignaturePadStrokePath({ points: live.points }) : \"\"\n    )\n  }\n\n  const schedulePaint = () => {\n    if (!frameRef.current) frameRef.current = requestAnimationFrame(paint)\n  }\n\n  const addSample = (\n    live: LiveStroke,\n    clientX: number,\n    clientY: number,\n    pressure: number,\n    time: number\n  ) => {\n    const x = (clientX - live.left) * live.scaleX\n    const y = (clientY - live.top) * live.scaleY\n    live.rawX = x\n    live.rawY = y\n    const { minWidth, maxWidth, sizing, smoothing } = config\n    const usePressure =\n      sizing === \"pressure\" || (sizing === \"auto\" && live.pointerType === \"pen\")\n    const pressed = minWidth + (maxWidth - minWidth) * Math.min(pressure, 1)\n    const previous = live.points[live.points.length - 1]\n\n    if (!previous) {\n      /* A pen at rest is slow, so it starts near full width; at the midpoint\n         a tap left a dot a third the weight of the line beside it. */\n      live.size = usePressure ? pressed : minWidth + (maxWidth - minWidth) * 0.8\n      live.time = time\n      live.points.push([round(x), round(y), round(live.size)])\n      return\n    }\n\n    /* Streamline: each point moves only part of the way toward the pointer. */\n    const follow = 1 - smoothing * 0.85\n    const sx = previous[0] + (x - previous[0]) * follow\n    const sy = previous[1] + (y - previous[1]) * follow\n    const distance = Math.hypot(sx - previous[0], sy - previous[1])\n    if (distance < 0.75) return\n\n    const speed = distance / Math.max(time - live.time, 1)\n    live.velocity = live.velocity * 0.3 + speed * 0.7\n    live.time = time\n    const target = usePressure\n      ? pressed\n      : Math.max(maxWidth / (live.velocity + 1), minWidth)\n    live.size += (target - live.size) * 0.35\n    live.points.push([round(sx), round(sy), round(live.size)])\n  }\n\n  /** `commit: false` discards: a pointercancel is the platform taking the pointer back, often a rejected palm. */\n  const finish = (pointerId: number, commit = true) => {\n    const live = liveRef.current\n    if (!live || live.pointerId !== pointerId) return\n    liveRef.current = null\n    cancelAnimationFrame(frameRef.current)\n    frameRef.current = 0\n    /* Cleared in the same discrete event that commits the stroke, so React\n       paints the committed path in this frame and the ink never blinks. */\n    pathRef.current?.setAttribute(\"d\", \"\")\n    config.setDrawing(false)\n    if (!commit) return\n    const last = live.points[live.points.length - 1]\n    if (last && Math.hypot(live.rawX - last[0], live.rawY - last[1]) >= 0.5) {\n      live.points.push([round(live.rawX), round(live.rawY), round(live.size)])\n    }\n    config.commitStroke(\n      config.color === undefined\n        ? { points: live.points }\n        : { points: live.points, color: config.color }\n    )\n  }\n\n  const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n    onPointerDown?.(event)\n    const area = event.currentTarget\n    const pointerType = event.pointerType as SignaturePadPointerType\n    if (\n      event.defaultPrevented ||\n      !config.interactive ||\n      event.target !== area ||\n      !event.isPrimary ||\n      (pointerType === \"mouse\" && event.button !== 0) ||\n      (config.pointerTypes && !config.pointerTypes.includes(pointerType))\n    ) {\n      return\n    }\n\n    /* A pen landing mid-stroke means the touch stroke was a resting palm. */\n    const live = liveRef.current\n    if (live) {\n      if (live.pointerType !== \"touch\" || pointerType !== \"pen\") return\n      liveRef.current = null\n    }\n\n    /* No preventDefault: the native mousedown focuses the area without\n       `:focus-visible`, where a scripted focus() lit the keyboard ring on\n       every mouse stroke. Touch scrolling and text selection are already off\n       through `touch-none` and `select-none`. */\n    try {\n      area.setPointerCapture(event.pointerId)\n    } catch {\n      /* Only a pointer that is no longer active refuses capture. */\n    }\n\n    const rect = area.getBoundingClientRect()\n    const next: LiveStroke = {\n      pointerId: event.pointerId,\n      pointerType,\n      points: [],\n      /* The ink layer starts inside the border, and a transformed ancestor\n         (a zooming dialog) scales the rect but not the coordinate space. */\n      left: rect.left + area.clientLeft * (rect.width / area.offsetWidth || 1),\n      top: rect.top + area.clientTop * (rect.height / area.offsetHeight || 1),\n      scaleX: rect.width ? area.offsetWidth / rect.width : 1,\n      scaleY: rect.height ? area.offsetHeight / rect.height : 1,\n      time: event.timeStamp,\n      velocity: 0,\n      size: 0,\n      rawX: 0,\n      rawY: 0,\n    }\n    liveRef.current = next\n    addSample(\n      next,\n      event.clientX,\n      event.clientY,\n      event.pressure,\n      event.timeStamp\n    )\n    schedulePaint()\n    config.setDrawing(true)\n    config.onStrokeStart?.({ pointerType })\n  }\n\n  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n    onPointerMove?.(event)\n    const live = liveRef.current\n    if (!live || live.pointerId !== event.pointerId) return\n    /* A fast pen reports several samples per frame; the coalesced list keeps\n       curves round instead of cutting across them. */\n    const samples = event.nativeEvent.getCoalescedEvents?.() ?? []\n    for (const sample of samples.length ? samples : [event.nativeEvent]) {\n      addSample(\n        live,\n        sample.clientX,\n        sample.clientY,\n        sample.pressure,\n        sample.timeStamp\n      )\n    }\n    schedulePaint()\n  }\n\n  const handleKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event)\n    if (event.defaultPrevented || !config.interactive) return\n    if (!(event.metaKey || event.ctrlKey) || event.altKey) return\n    /* The physical key when the layout types no Latin letter (Cyrillic,\n       Greek, Hebrew), so the shortcut survives a layout switch. */\n    const typed = event.key.toLowerCase()\n    const key = /^[a-z]$/.test(typed)\n      ? typed\n      : event.code.replace(/^Key/, \"\").toLowerCase()\n    /* Pressed on a pinned control, the action may disable that control\n       (Undo at the start of history, Clear or Save on an emptied pad), and\n       focus would fall to <body>; it moves to the area instead. */\n    const fromControl = event.target !== event.currentTarget\n    if (key === \"z\" && !event.shiftKey && api.canUndo) {\n      event.preventDefault()\n      api.undo()\n      if (fromControl) api.focus()\n    } else if (\n      (key === \"y\" || (key === \"z\" && event.shiftKey)) &&\n      api.canRedo\n    ) {\n      event.preventDefault()\n      api.redo()\n      if (fromControl) api.focus()\n    }\n  }\n\n  const labelled = props[\"aria-label\"] ?? props[\"aria-labelledby\"]\n  const statusId = useId()\n\n  return (\n    <div\n      ref={(node) => {\n        config.setArea(node)\n        if (typeof ref === \"function\") ref(node)\n        else if (ref) ref.current = node\n      }}\n      role=\"application\"\n      aria-roledescription=\"signature pad\"\n      aria-label={labelled ? undefined : \"Signature pad\"}\n      aria-disabled={api.disabled || undefined}\n      aria-describedby={describedBy ? `${describedBy} ${statusId}` : statusId}\n      tabIndex={config.interactive ? 0 : -1}\n      data-slot=\"signature-pad-area\"\n      data-variant={variant}\n      data-empty={api.isEmpty || undefined}\n      data-drawing={api.isDrawing || undefined}\n      data-disabled={api.disabled || undefined}\n      data-readonly={api.readOnly || undefined}\n      className={cn(\n        AREA_BASE_CLASS,\n        AREA_STATE_CLASS,\n        AREA_VARIANT_CLASS[variant],\n        className\n      )}\n      {...props}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={(event) => {\n        onPointerUp?.(event)\n        finish(event.pointerId)\n      }}\n      onPointerCancel={(event) => {\n        onPointerCancel?.(event)\n        finish(event.pointerId, false)\n      }}\n      onLostPointerCapture={(event) => {\n        onLostPointerCapture?.(event)\n        finish(event.pointerId)\n      }}\n      onKeyDown={handleKeyDown}\n      onContextMenu={(event) => {\n        onContextMenu?.(event)\n        /* A long press on touch opens the context menu mid-stroke. */\n        if (liveRef.current) event.preventDefault()\n      }}\n    >\n      {children}\n      {/* The ink is invisible to assistive tech, so its state is spoken here. */}\n      <span id={statusId} className=\"sr-only\">\n        {api.isEmpty ? \"Empty\" : \"Signed\"}\n        {api.readOnly ? \", read only\" : \"\"}\n      </span>\n      <svg\n        data-slot=\"signature-pad-canvas\"\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 size-full overflow-visible\"\n      >\n        {api.strokes.map((stroke, index) => (\n          <StrokePath key={index} stroke={stroke} />\n        ))}\n        <path ref={pathRef} fill={config.color ?? \"currentColor\"} />\n      </svg>\n      {config.name && (\n        <input\n          ref={inputRef}\n          type=\"text\"\n          name={config.name}\n          form={config.form}\n          required={config.required}\n          disabled={api.disabled}\n          defaultValue=\"\"\n          tabIndex={-1}\n          aria-hidden=\"true\"\n          /* A failed `required` check focuses this input; hand that to the pad. */\n          onFocus={() => api.focus()}\n          className=\"pointer-events-none absolute bottom-0 left-1/2 size-px opacity-0\"\n        />\n      )}\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Overlays                                  */\n/* -------------------------------------------------------------------------- */\n\n/** A dashed signing line with a cross at its start. Replace the cross with `children`. */\nfunction SignaturePadGuide({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"signature-pad-guide\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"text-muted-foreground border-muted-foreground/40 pointer-events-none absolute inset-x-6 bottom-8 flex items-end gap-2 border-b border-dashed pb-1.5 text-xs\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? (\n        <svg\n          viewBox=\"0 0 12 12\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.5\"\n          strokeLinecap=\"round\"\n          className=\"size-3\"\n        >\n          <path d=\"M2 2l8 8M10 2l-8 8\" />\n        </svg>\n      )}\n    </div>\n  )\n}\n\n/** Shown only while the pad is empty and nobody is drawing. */\nfunction SignaturePadPlaceholder({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) {\n  const { isEmpty, isDrawing } = useSignaturePad()\n  if (!isEmpty || isDrawing) return null\n\n  return (\n    <div\n      data-slot=\"signature-pad-placeholder\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"text-muted-foreground pointer-events-none absolute inset-0 flex items-center justify-center p-4 text-center text-sm\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? \"Sign here\"}\n    </div>\n  )\n}\n\nconst CONTROLS_POSITION_CLASS = {\n  \"top-start\": \"top-2 start-2\",\n  \"top-end\": \"top-2 end-2\",\n  \"bottom-start\": \"bottom-2 start-2\",\n  \"bottom-end\": \"bottom-2 end-2\",\n}\n\n/**\n * Pins a group of controls to a corner of the area, above the ink. Top-end by\n * default: signatures run along the bottom, where `bottom-end` sat on the\n * guide line and under the last letters.\n */\nfunction SignaturePadControls({\n  position = \"top-end\",\n  className,\n  ...props\n}: ComponentProps<\"div\"> & {\n  position?: keyof typeof CONTROLS_POSITION_CLASS\n}) {\n  return (\n    <div\n      data-slot=\"signature-pad-controls\"\n      data-position={position}\n      className={cn(\n        \"absolute z-10 flex items-center gap-1\",\n        CONTROLS_POSITION_CLASS[position],\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Controls                                  */\n/* -------------------------------------------------------------------------- */\n\ntype SignaturePadControlProps = ComponentProps<typeof Button>\n\n/*\n * Each control defaults to an icon button and takes text as `children`. A\n * control that disables itself would strand keyboard focus on `<body>`, so\n * each one hands focus to the area when its own action empties it.\n */\nfunction SignaturePadClear({\n  children,\n  onClick,\n  disabled,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: SignaturePadControlProps) {\n  const api = useSignaturePad()\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"signature-pad-clear\"\n      aria-label={children === undefined ? \"Clear signature\" : undefined}\n      variant={variant}\n      size={size}\n      {...props}\n      disabled={disabled || api.isEmpty || api.disabled || api.readOnly}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        api.clear()\n        api.focus()\n      }}\n    >\n      {children ?? (\n        <IconPlaceholder\n          lucide=\"BrushCleaningIcon\"\n          tabler=\"IconEraser\"\n          hugeicons=\"BrushCleaningIcon\"\n          phosphor=\"BroomIcon\"\n          remixicon=\"RiEraserLine\"\n          aria-hidden=\"true\"\n        />\n      )}\n    </Button>\n  )\n}\n\nfunction SignaturePadUndo({\n  children,\n  onClick,\n  disabled,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: SignaturePadControlProps) {\n  const api = useSignaturePad()\n  const { undoDepth } = useSignaturePadConfig(\"SignaturePadUndo\")\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"signature-pad-undo\"\n      aria-label={children === undefined ? \"Undo\" : undefined}\n      variant={variant}\n      size={size}\n      {...props}\n      disabled={disabled || !api.canUndo || api.disabled || api.readOnly}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        api.undo()\n        if (undoDepth === 1) api.focus()\n      }}\n    >\n      {children ?? (\n        <IconPlaceholder\n          lucide=\"Undo2Icon\"\n          tabler=\"IconArrowBackUp\"\n          hugeicons=\"Undo02Icon\"\n          phosphor=\"ArrowUUpLeftIcon\"\n          remixicon=\"RiArrowGoBackLine\"\n          aria-hidden=\"true\"\n        />\n      )}\n    </Button>\n  )\n}\n\nfunction SignaturePadRedo({\n  children,\n  onClick,\n  disabled,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: SignaturePadControlProps) {\n  const api = useSignaturePad()\n  const { redoDepth } = useSignaturePadConfig(\"SignaturePadRedo\")\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"signature-pad-redo\"\n      aria-label={children === undefined ? \"Redo\" : undefined}\n      variant={variant}\n      size={size}\n      {...props}\n      disabled={disabled || !api.canRedo || api.disabled || api.readOnly}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        api.redo()\n        if (redoDepth === 1) api.focus()\n      }}\n    >\n      {children ?? (\n        <IconPlaceholder\n          lucide=\"Redo2Icon\"\n          tabler=\"IconArrowForwardUp\"\n          hugeicons=\"Redo02Icon\"\n          phosphor=\"ArrowUUpRightIcon\"\n          remixicon=\"RiArrowGoForwardLine\"\n          aria-hidden=\"true\"\n        />\n      )}\n    </Button>\n  )\n}\n\n/** Serializes the pad and hands the result to `onSave`. Disabled while the pad is empty. */\nfunction SignaturePadSave({\n  format = \"png\",\n  options,\n  onSave,\n  children,\n  onClick,\n  disabled,\n  variant = \"outline\",\n  size = \"icon-sm\",\n  ...props\n}: SignaturePadControlProps & {\n  format?: SignaturePadFormat\n  options?: SignaturePadExportOptions\n  onSave?: (value: string, strokes: SignaturePadStroke[]) => void\n}) {\n  const api = useSignaturePad()\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"signature-pad-save\"\n      aria-label={children === undefined ? \"Save signature\" : undefined}\n      variant={variant}\n      size={size}\n      {...props}\n      disabled={disabled || api.isEmpty || api.disabled}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented) return\n        onSave?.(api.serialize(format, options), api.strokes)\n      }}\n    >\n      {children ?? (\n        <IconPlaceholder\n          lucide=\"SaveIcon\"\n          tabler=\"IconDeviceFloppy\"\n          hugeicons=\"FloppyDiskIcon\"\n          phosphor=\"FloppyDiskIcon\"\n          remixicon=\"RiSaveLine\"\n          aria-hidden=\"true\"\n        />\n      )}\n    </Button>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Preview                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Saved strokes as an inline SVG cropped to the ink, for lists, receipts and\n * documents. Needs no `SignaturePad` around it, renders on the server, and\n * scales with CSS while the ink keeps following `currentColor`.\n */\nfunction SignaturePadPreview({\n  strokes,\n  padding = 4,\n  color,\n  className,\n  ...props\n}: Omit<ComponentProps<\"svg\">, \"color\"> & {\n  strokes: SignaturePadStroke[]\n  padding?: number\n  color?: string\n}) {\n  if (strokes.length === 0) return null\n  const { x, y, width, height } = resolveFrame(strokes, { padding })\n\n  return (\n    <svg\n      data-slot=\"signature-pad-preview\"\n      role=\"img\"\n      aria-label=\"Signature\"\n      viewBox={`${x} ${y} ${width} ${height}`}\n      width={width}\n      height={height}\n      className={cn(\"text-foreground h-auto max-w-full\", className)}\n      {...props}\n    >\n      {strokes.map((stroke, index) => (\n        <path\n          key={index}\n          d={getSignaturePadStrokePath(stroke)}\n          fill={stroke.color ?? color ?? \"currentColor\"}\n        />\n      ))}\n    </svg>\n  )\n}\n\nexport {\n  SignaturePad,\n  SignaturePadArea,\n  SignaturePadGuide,\n  SignaturePadPlaceholder,\n  SignaturePadControls,\n  SignaturePadClear,\n  SignaturePadUndo,\n  SignaturePadRedo,\n  SignaturePadSave,\n  SignaturePadPreview,\n  useSignaturePad,\n  getSignaturePadStrokePath,\n  getSignaturePadBounds,\n  serializeSignaturePad,\n  signaturePadToSVG,\n  signaturePadToDataURL,\n  signaturePadToBlob,\n}","target":"components/reui/signature-pad.tsx"}]}