{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "description": "The box at the bottom of a form where somebody signs with a finger, a mouse or a stylus — and, beside it, the field where somebody who cannot draw types their name instead. Reach for it wherever a screen asks for assent that is meant to bind: a delivery or handover receipt, a rental or equipment checkout, a treatment or research consent form, a waiver and liability release, a visitor or contractor sign-in at a front desk, a timesheet or job-completion sheet a technician gets signed on a tablet, a lease or invoice approval, a school permission slip, and the light end of e-signature where a full contract platform is far more than the job needs. Common asks it answers: \"signature pad react\", \"react signature canvas\", \"draw signature component\", \"e-signature input react\", \"sign here box react\", \"capture signature on tablet\", \"signature-pad alternative\", \"react-signature-canvas alternative\", \"shadcn signature pad\", \"shadcn signature input\", \"canvas drawing react component\", \"why is my canvas blurry\", \"canvas blurry on retina\", \"devicePixelRatio canvas react\", \"canvas high dpi scaling\", \"smooth line drawing canvas\", \"canvas drawing looks jagged\", \"pointermove skipping points\", \"getCoalescedEvents react\", \"line breaks when mouse leaves canvas\", \"setPointerCapture drawing\", \"canvas drawing scrolls page on mobile\", \"touch-action none canvas\", \"signature to png data url\", \"export canvas as svg\", \"trim whitespace around signature\", \"transparent signature png invisible\", \"accessible signature field\", \"signature pad screen reader\", \"canvas accessibility alternative\", \"署名 パッド react\", \"サイン 入力 canvas\", \"canvas がぼやける retina\". Official shadcn/ui has nothing to build this from, and the measurement is not close: fetching all sixty-three registry entries today (sixty-two are fetchable — questionnaire is listed and 404s on both style tracks) and grepping 240 KB of source, getContext, toDataURL, toBlob, pointerdown, pointermove, setPointerCapture, getCoalescedEvents, devicePixelRatio, beginPath, lineTo, quadraticCurveTo, touch-action and signature are every one of them a zero hit. The only match for canvas at all is ten occurrences in sidebar, and every one is the Tailwind variant name offcanvas. There is no component in official shadcn that draws — not one — so an agent asked for a signature field builds it from a bare canvas and a mousemove listener, and the four things that make this hard are exactly the four it will get wrong. The first is that a canvas has two sizes and the wrong one is the obvious one. The CSS size is how big the element looks; the width and height attributes are how many pixels actually exist, and they stay at 300x150 no matter what the stylesheet says. Left alone, a phone at 3x renders the box at a third of its own resolution and scales the result up, so the strokes come out soft — and a signature is a thin line whose weight and wobble are the whole of what identifies it, which makes this the one component where blur is not cosmetic. The attributes are set to the CSS size times the device pixel ratio, capped so a 4x screen does not allocate sixteen pixels of memory per CSS pixel, and the context is set back to CSS coordinates with setTransform rather than scale — scale multiplies into the transform already there, so a pad that survives two resizes draws at 4x and the signature walks off the box. The second is that pointermove is not the pointer. One move event is delivered per animation frame, but the digitiser sampled the pen many times inside that frame and the browser keeps the ones it skipped. A quick signature is where it shows: at 60Hz a fast flick is four or five points and comes out as a zigzag, while getCoalescedEvents returns the twenty that were really seen. Safari has been observed returning an empty list, so the event itself is the fallback rather than the assumption. Sampling alone is still not enough, because the corners in a joined-up polyline are in the sample rate rather than in the hand: each sample becomes the control point of a quadratic and the curve runs through the midpoints between them, so the line is tangent to the path the hand took and has no corners of its own. During a stroke only the newest segment is drawn, so a long signature costs the same per sample as a short one. The third is that a canvas is a blank to a screen reader, and no aria-label fixes it — the label names the box, and the task is to make a mark inside it. Drawing is a pointer gesture, so a pad that only draws is a form that a keyboard, switch or screen reader user cannot complete, on precisely the documents where being unable to complete it has consequences. A typed full name is the equivalent that is already recognised in practice, so it sits beside the box as a real labelled field rather than as a fallback bolted on: either path produces a value and an image, typing renders in a script face on the canvas so a sighted user sees the mark too, and the two are mutually exclusive because there is one signature. required is handled the same way round: it goes on the typed field while nothing is signed and lifts the moment something is drawn — never on the hidden input, which is barred from constraint validation outright, so the attribute parses, the browser ignores it, and the form submits unsigned. The fourth is that the line breaks when the hand leaves the box, which is where the descender of a real signature goes. setPointerCapture keeps the events coming until the pointer lifts, wherever that happens; the capture is checked before being released, because releasing one that pointercancel already took throws; a second finger landing mid-stroke is ignored rather than allowed to overwrite the stroke in progress; and touch-action is none, or the first downward stroke on a phone scrolls the page instead of drawing while the browser waits to find out which was meant. Strokes are kept as data, not as pixels, which is what makes the rest work. Undo is a slice. A resize — a sidebar opening, a tab becoming visible, a container query firing, none of them a window resize — is observed with a ResizeObserver and replayed, where a pixel-only pad loses the signature to the attribute assignment that resizes it. And the export is resolution-independent: signatureToSvg emits a real SVG document, because a signature is stored small and shown large, printed onto a contract or scaled into a PDF, and a raster of a 160-pixel box has one resolution forever. trim crops to the ink so a stored signature is not mostly empty box, with the bound taken over the Bézier control points — a quadratic stays inside the triangle of its own three, so it is exact without solving the curve — plus half the pen width, without which the trim slices the outermost stroke in half. toDataURL is there for the APIs that want a raster, and its background is documented rather than assumed: a transparent PNG of black ink is invisible the moment it lands on anything dark. Every string that reaches the SVG is escaped, since a name is free text and a document built by concatenation is the oldest bug there is. The API: value or defaultValue as a discriminated { type: \"drawn\", strokes, width, height } or { type: \"typed\", name } — the drawn form carries the box it was drawn in, because strokes are CSS pixels and without the box they cannot be laid out again on the receipt screen that shows them later. Plus onChange, penColor which defaults to the theme's own text colour, penWidth, height, maxPixelRatio, disabled, allowTyped, required, and name to post the signature through a plain HTML form as an SVG data URL. The ref exposes clear, undo, isEmpty, getValue, toSVG and toDataURL for a submit handler. strokeGeometry, strokePathData, signatureBounds, isSignatureEmpty, signatureToSvg and backingSize are exported as pure functions, so a stored signature can be rendered on a server that has no canvas at all. Within pulld it is the first component that draws: file-dropzone takes an image in and upload-list lists what arrived, but nothing until now made a mark. It sits next to type-to-confirm, which is the other way a screen asks somebody to mean it — typing a phrase to authorise a destructive action, where this captures assent that gets stored and shown back. One file, zero dependencies, not even an icon, and every colour is a shadcn token, so light and dark follow on their own.",
  "files": [
    {
      "path": "registry/ui/signature-pad.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/** One sampled point of a stroke, in CSS pixels from the top-left of the drawing box. */\nexport interface SignaturePoint {\n  x: number\n  y: number\n}\n\n/** One continuous press-drag-release. A tap that never moved is a stroke of one point. */\nexport type SignatureStroke = SignaturePoint[]\n\n/**\n * What was signed, in the two forms a signature can take here.\n *\n * The drawn form carries the box it was drawn in. The strokes are CSS pixels, not fractions, so\n * without the box they cannot be laid out again — and a signature is stored to be shown later, in\n * a receipt, a PDF or an audit screen that is never the width of the pad it was drawn on.\n */\nexport type SignatureValue =\n  | { type: \"drawn\"; strokes: SignatureStroke[]; width: number; height: number }\n  | { type: \"typed\"; name: string }\n\n/** The geometry of one stroke: a dot, or a start point and the curve segments after it. */\nexport type StrokeGeometry =\n  | { kind: \"dot\"; x: number; y: number }\n  | { kind: \"path\"; start: SignaturePoint; segments: SignatureSegment[] }\n\n/** A quadratic segment: control point `c`, end point `x`/`y`. */\nexport interface SignatureSegment {\n  cx: number\n  cy: number\n  x: number\n  y: number\n}\n\nexport interface SignaturePadProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"defaultValue\"> {\n  /** Accessible name for the whole control, e.g. \"Signature\". Rendered as its label. */\n  label?: React.ReactNode\n  /** Height of the drawing box in CSS pixels (default 160). The width is fluid. */\n  height?: number\n  /** Signature for a controlled component. Pass `null` for \"not signed yet\". */\n  value?: SignatureValue | null\n  /** Starting signature for an uncontrolled component. */\n  defaultValue?: SignatureValue | null\n  /** Called when a stroke finishes, when the typed name changes, and on undo and clear. */\n  onChange?: (value: SignatureValue | null) => void\n  /** Ink colour. Any CSS colour; defaults to the theme's foreground. */\n  penColor?: string\n  /** Ink width in CSS pixels (default 2). */\n  penWidth?: number\n  /**\n   * Ceiling on the device pixel ratio the backing store is drawn at (default 3).\n   *\n   * A 4x phone would otherwise allocate sixteen pixels of memory per CSS pixel to render a line\n   * nobody can see the difference in.\n   */\n  maxPixelRatio?: number\n  /** Turns off drawing and typing, and dims the box. */\n  disabled?: boolean\n  /**\n   * Whether to offer the typed-name field (default true).\n   *\n   * Turning it off leaves the signature reachable by pointer only. See the note on the component.\n   */\n  allowTyped?: boolean\n  /**\n   * Submits the signature with a plain HTML form under this name, as an `image/svg+xml` data URL\n   * for a drawn signature and as the plain text for a typed one.\n   */\n  name?: string\n  /** Marks the field required, both for the form and for assistive technology. */\n  required?: boolean\n  /** Instruction under the box. Defaults to a sentence naming all three input devices. */\n  hint?: React.ReactNode\n  /** Label for the typed-name field. */\n  typedLabel?: React.ReactNode\n  /** Text between the two ways of signing. */\n  dividerLabel?: React.ReactNode\n  /** Labels for the two buttons and the announcements, for translation. */\n  labels?: Partial<typeof DEFAULT_LABELS>\n  /** Classes for the drawing box itself. */\n  canvasClassName?: string\n}\n\n/** What the imperative ref exposes, for a submit handler that needs the image. */\nexport interface SignaturePadHandle {\n  /** Removes every stroke and the typed name. */\n  clear: () => void\n  /** Removes the last stroke. Does nothing to a typed name. */\n  undo: () => void\n  /** Whether nothing has been signed. */\n  isEmpty: () => boolean\n  /** The current value, the same object `onChange` last reported. */\n  getValue: () => SignatureValue | null\n  /** The signature as an SVG document, or `null` when empty. */\n  toSVG: (options?: SignatureExportOptions) => string | null\n  /** The signature as a raster data URL, or `null` when empty. Browser only. */\n  toDataURL: (options?: SignatureRasterOptions) => string | null\n}\n\nexport interface SignatureExportOptions {\n  /** Ink colour. Defaults to the pen colour in use. */\n  penColor?: string\n  /** Ink width. Defaults to the pen width in use. */\n  penWidth?: number\n  /**\n   * Colour painted behind the ink. Defaults to none — see the note on `toDataURL` about what\n   * a transparent signature does when it lands somewhere dark.\n   */\n  backgroundColor?: string\n  /** Crops to the ink plus this much padding, instead of keeping the whole box. */\n  trim?: boolean | number\n}\n\nexport interface SignatureRasterOptions extends SignatureExportOptions {\n  /** MIME type, e.g. `\"image/jpeg\"` (default `\"image/png\"`). */\n  type?: string\n  /** Quality for lossy types, 0–1. */\n  quality?: number\n  /** Pixels per CSS pixel in the output (default 2). */\n  scale?: number\n}\n\nconst DEFAULT_LABELS = {\n  clear: \"Clear\",\n  undo: \"Undo\",\n  /** Announced once when a stroke or a name lands, not on every sample. */\n  signed: \"Signature captured.\",\n  signedAs: (name: string) => `Signed as ${name}.`,\n  cleared: \"Signature cleared.\",\n}\n\nconst DEFAULT_HEIGHT = 160\nconst DEFAULT_PEN_WIDTH = 2\nconst DEFAULT_MAX_PIXEL_RATIO = 3\nconst TRIM_PADDING = 8\n\n/**\n * The font a typed name is drawn in. A typed signature that renders in the page's body font reads\n * as a form field someone forgot to style rather than as a mark someone made, so the fallbacks run\n * through the script faces the three desktop platforms ship before giving up on `cursive`.\n */\nconst SCRIPT_FONT =\n  '\"Segoe Script\", \"Bradley Hand\", \"Snell Roundhand\", \"Apple Chancery\", cursive'\n\nconst midpoint = (a: SignaturePoint, b: SignaturePoint): SignaturePoint => ({\n  x: (a.x + b.x) / 2,\n  y: (a.y + b.y) / 2,\n})\n\n/**\n * The curve through one stroke's sampled points.\n *\n * A stroke is a list of places a pointer was seen, and joining them with straight lines is what\n * makes a fast signature come out as a polygon — the corners are not in the hand, they are in the\n * sampling. Each sampled point becomes the control point of a quadratic instead, and the curve\n * passes through the midpoints between them, so the drawn line is tangent to the path the hand\n * took and has no corners of its own.\n *\n * The last segment is a quadratic whose control point is its own end, which is a straight line —\n * so the stroke reaches the final sample exactly rather than stopping half a sample short of where\n * the pen came up. One shape for every segment keeps this function, the incremental drawing during\n * a stroke and the SVG export provably in agreement: all three consume this list.\n */\nexport function strokeGeometry(points: SignatureStroke): StrokeGeometry | null {\n  if (!points || points.length === 0) return null\n  if (points.length === 1) return { kind: \"dot\", x: points[0].x, y: points[0].y }\n\n  const segments: SignatureSegment[] = []\n  for (let i = 1; i < points.length - 1; i++) {\n    const end = midpoint(points[i], points[i + 1])\n    segments.push({ cx: points[i].x, cy: points[i].y, x: end.x, y: end.y })\n  }\n  const last = points[points.length - 1]\n  segments.push({ cx: last.x, cy: last.y, x: last.x, y: last.y })\n  return { kind: \"path\", start: points[0], segments }\n}\n\n/** Trims trailing zeros off a rounded number so the SVG path stays short. */\nconst num = (n: number): string => String(Math.round(n * 100) / 100)\n\n/** The `d` attribute for one stroke, or `null` for a stroke that is a dot or is empty. */\nexport function strokePathData(points: SignatureStroke): string | null {\n  const geometry = strokeGeometry(points)\n  if (!geometry || geometry.kind !== \"path\") return null\n  let d = `M${num(geometry.start.x)} ${num(geometry.start.y)}`\n  for (const s of geometry.segments) d += `Q${num(s.cx)} ${num(s.cy)} ${num(s.x)} ${num(s.y)}`\n  return d\n}\n\n/** Whether anything has actually been signed. A blank or whitespace-only name has not. */\nexport function isSignatureEmpty(value: SignatureValue | null | undefined): boolean {\n  if (!value) return true\n  if (value.type === \"typed\") return value.name.trim().length === 0\n  return value.strokes.every((stroke) => stroke.length === 0)\n}\n\n/**\n * The box the ink occupies, in the coordinates of the drawing box, or `null` when there is none.\n *\n * A quadratic Bézier stays inside the triangle of its own start, control and end points, so taking\n * the extremes of those three is a true bound without solving for the curve at all — which is the\n * only reason a smoothed stroke can be trimmed cheaply. Half the pen width is added on every side\n * because the line is drawn centred on the path, and a round cap puts that much ink past the final\n * point too. Without that the trimmed export clips exactly half the outermost stroke, which on a\n * signature is the flourish somebody looks at to recognise it.\n */\nexport function signatureBounds(\n  strokes: SignatureStroke[],\n  penWidth: number = DEFAULT_PEN_WIDTH\n): { x: number; y: number; width: number; height: number } | null {\n  let minX = Infinity\n  let minY = Infinity\n  let maxX = -Infinity\n  let maxY = -Infinity\n\n  const include = (x: number, y: number) => {\n    if (!Number.isFinite(x) || !Number.isFinite(y)) return\n    if (x < minX) minX = x\n    if (x > maxX) maxX = x\n    if (y < minY) minY = y\n    if (y > maxY) maxY = y\n  }\n\n  for (const stroke of strokes) {\n    const geometry = strokeGeometry(stroke)\n    if (!geometry) continue\n    if (geometry.kind === \"dot\") {\n      include(geometry.x, geometry.y)\n      continue\n    }\n    include(geometry.start.x, geometry.start.y)\n    for (const s of geometry.segments) {\n      include(s.cx, s.cy)\n      include(s.x, s.y)\n    }\n  }\n\n  if (minX === Infinity) return null\n  const pad = Math.max(penWidth, 0) / 2\n  return {\n    x: minX - pad,\n    y: minY - pad,\n    width: maxX - minX + pad * 2,\n    height: maxY - minY + pad * 2,\n  }\n}\n\n/**\n * Escapes text for an XML attribute or text node.\n *\n * The typed name reaches the SVG as text and the colours reach it as attributes, and all three are\n * strings somebody else supplied. A name with an ampersand in it is enough to make the document\n * fail to parse, and a name that closes its own tag is worse than that — an SVG is a document, and\n * the one place a signature ends up is a page that renders it back.\n */\nconst escapeXml = (s: string): string =>\n  s.replace(/[&<>\"']/g, (c) =>\n    c === \"&\" ? \"&amp;\" : c === \"<\" ? \"&lt;\" : c === \">\" ? \"&gt;\" : c === '\"' ? \"&quot;\" : \"&#39;\"\n  )\n\n/**\n * The signature as a standalone SVG document, or `null` when nothing is signed.\n *\n * SVG rather than a PNG is the default here because a signature is stored small and shown large:\n * printed onto a contract, scaled into a PDF, zoomed by somebody checking it. A raster of a 160px\n * box has one resolution forever, and it is the resolution of the pad it was drawn on.\n */\nexport function signatureToSvg(\n  value: SignatureValue | null | undefined,\n  {\n    width = 320,\n    height = DEFAULT_HEIGHT,\n    penColor = \"#000000\",\n    penWidth = DEFAULT_PEN_WIDTH,\n    backgroundColor,\n    trim = false,\n  }: SignatureExportOptions & { width?: number; height?: number } = {}\n): string | null {\n  if (isSignatureEmpty(value) || !value) return null\n\n  const open = (w: number, h: number, viewBox: string) =>\n    `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${num(w)}\" height=\"${num(h)}\" viewBox=\"${viewBox}\">` +\n    (backgroundColor\n      ? `<rect width=\"100%\" height=\"100%\" fill=\"${escapeXml(backgroundColor)}\"/>`\n      : \"\")\n\n  if (value.type === \"typed\") {\n    const name = value.name.trim()\n    // Sized off the box rather than measured: this function has no text metrics, and a signature\n    // that renders slightly small is a better failure than one that needs a DOM to exist at all.\n    const size = Math.min(height * 0.42, (width * 1.6) / Math.max(name.length, 1))\n    return (\n      open(width, height, `0 0 ${num(width)} ${num(height)}`) +\n      `<text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dominant-baseline=\"middle\"` +\n      ` font-family=\"${escapeXml(SCRIPT_FONT)}\" font-size=\"${num(size)}\"` +\n      ` fill=\"${escapeXml(penColor)}\">${escapeXml(name)}</text></svg>`\n    )\n  }\n\n  const box = trim === false ? null : signatureBounds(value.strokes, penWidth)\n  const pad = typeof trim === \"number\" ? trim : TRIM_PADDING\n  const viewBox = box\n    ? `${num(box.x - pad)} ${num(box.y - pad)} ${num(box.width + pad * 2)} ${num(box.height + pad * 2)}`\n    : `0 0 ${num(value.width)} ${num(value.height)}`\n  const outW = box ? box.width + pad * 2 : value.width\n  const outH = box ? box.height + pad * 2 : value.height\n\n  let body = \"\"\n  for (const stroke of value.strokes) {\n    const geometry = strokeGeometry(stroke)\n    if (!geometry) continue\n    if (geometry.kind === \"dot\") {\n      body += `<circle cx=\"${num(geometry.x)}\" cy=\"${num(geometry.y)}\" r=\"${num(penWidth / 2)}\" fill=\"${escapeXml(penColor)}\"/>`\n    } else {\n      body += `<path d=\"${strokePathData(stroke)}\" fill=\"none\" stroke=\"${escapeXml(penColor)}\" stroke-width=\"${num(penWidth)}\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>`\n    }\n  }\n  return open(outW, outH, viewBox) + body + \"</svg>\"\n}\n\n/**\n * The backing-store size for a box of `width` x `height` CSS pixels.\n *\n * A canvas has two sizes, and this is the one nearly every signature pad gets wrong. The CSS size\n * is how big the element is; the `width`/`height` attributes are how many pixels are actually\n * drawn, and they default to 300x150 regardless. Left alone, a 2x or 3x screen renders the box at\n * a third of its own resolution and then scales it up — and the thing that suffers most is a thin\n * line, which is the entire content here. A signature is identified by the weight and wobble of\n * its strokes, so blurring it is not a cosmetic loss.\n */\nexport function backingSize(\n  width: number,\n  height: number,\n  pixelRatio: number,\n  maxPixelRatio: number = DEFAULT_MAX_PIXEL_RATIO\n): { width: number; height: number; ratio: number } {\n  const ratio = Math.min(Math.max(Number.isFinite(pixelRatio) ? pixelRatio : 1, 1), maxPixelRatio)\n  return {\n    width: Math.max(1, Math.round(width * ratio)),\n    height: Math.max(1, Math.round(height * ratio)),\n    ratio,\n  }\n}\n\nconst EMPTY_STROKES: SignatureStroke[] = []\n\nconst BUTTON_CLASS =\n  \"inline-flex h-7 items-center rounded-md border border-border bg-background px-2 text-xs font-medium ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\"\n\n/** Paints one stroke onto a 2D context that is already scaled to CSS pixels. */\nfunction paintStroke(ctx: CanvasRenderingContext2D, stroke: SignatureStroke, penWidth: number) {\n  const geometry = strokeGeometry(stroke)\n  if (!geometry) return\n  if (geometry.kind === \"dot\") {\n    ctx.beginPath()\n    ctx.arc(geometry.x, geometry.y, penWidth / 2, 0, Math.PI * 2)\n    ctx.fill()\n    return\n  }\n  ctx.beginPath()\n  ctx.moveTo(geometry.start.x, geometry.start.y)\n  for (const s of geometry.segments) ctx.quadraticCurveTo(s.cx, s.cy, s.x, s.y)\n  ctx.stroke()\n}\n\n/**\n * A place to sign: draw with a finger, mouse or pen, or type your name instead.\n *\n * ```tsx\n * const pad = React.useRef<SignaturePadHandle>(null)\n *\n * <SignaturePad\n *   label=\"Sign to confirm delivery\"\n *   name=\"signature\"\n *   required\n *   onChange={setSignature}\n *   ref={pad}\n * />\n * ```\n *\n * The typed field is not a convenience, and it is why this is a component rather than a canvas\n * with a listener on it. A canvas is a blank to a screen reader — there is nothing inside it to\n * read, and no `aria-label` changes that, because the label describes the box and the task is to\n * make a mark in it. Drawing is a pointer gesture, so a signature pad that only draws is a form\n * nobody using a keyboard, a switch or a screen reader can complete. Typing a full name is the\n * equivalent already recognised in practice and in law, so it is offered beside the box rather\n * than bolted on, and either one produces a value and an image. `allowTyped={false}` removes it,\n * and removes the only path some people have through the form; do it knowingly.\n */\nexport const SignaturePad = React.forwardRef<SignaturePadHandle, SignaturePadProps>(\n  function SignaturePad(\n    {\n      className,\n      label,\n      height = DEFAULT_HEIGHT,\n      value: valueProp,\n      defaultValue = null,\n      onChange,\n      penColor,\n      penWidth = DEFAULT_PEN_WIDTH,\n      maxPixelRatio = DEFAULT_MAX_PIXEL_RATIO,\n      disabled = false,\n      allowTyped = true,\n      name,\n      required = false,\n      hint,\n      typedLabel = \"Or type your full name to sign\",\n      dividerLabel = \"or\",\n      labels,\n      canvasClassName,\n      ...props\n    },\n    ref\n  ) {\n    const text = { ...DEFAULT_LABELS, ...labels }\n    const reactId = React.useId()\n    const labelId = `${reactId}-label`\n    const hintId = `${reactId}-hint`\n    const typedId = `${reactId}-typed`\n\n    const [uncontrolled, setUncontrolled] = React.useState<SignatureValue | null>(defaultValue)\n    const isControlled = valueProp !== undefined\n    const value = isControlled ? valueProp : uncontrolled\n\n    const canvasRef = React.useRef<HTMLCanvasElement | null>(null)\n    const boxRef = React.useRef<HTMLDivElement | null>(null)\n    /** The stroke being drawn right now, and where the last curve segment ended. */\n    const drawing = React.useRef<{ pointerId: number; points: SignatureStroke; from: SignaturePoint } | null>(null)\n    /** The resolved ink colour, read from the theme once the box is mounted. */\n    const [inkColor, setInkColor] = React.useState(penColor)\n    const [announcement, setAnnouncement] = React.useState(\"\")\n\n    const strokes = value?.type === \"drawn\" ? value.strokes : EMPTY_STROKES\n    const typedName = value?.type === \"typed\" ? value.name : \"\"\n    // Not \"currentColor\": a canvas context takes a colour, not a keyword from the cascade, and\n    // assigning one it cannot parse is silently ignored — leaving whatever was set before.\n    const ink = penColor ?? inkColor ?? \"#000000\"\n\n    const commit = React.useCallback(\n      (next: SignatureValue | null) => {\n        if (!isControlled) setUncontrolled(next)\n        onChange?.(next)\n      },\n      [isControlled, onChange]\n    )\n\n    // The pen colour defaults to whatever the theme paints text in, which means resolving a CSS\n    // custom property to something the canvas can use: a context takes a colour string and knows\n    // nothing about the cascade, so `--foreground` handed to `strokeStyle` is simply ignored and\n    // the ink comes out black — invisible in dark mode, which is exactly where nobody tests it.\n    React.useEffect(() => {\n      if (penColor || !boxRef.current) return\n      const resolved = getComputedStyle(boxRef.current).color\n      if (resolved) setInkColor(resolved)\n    }, [penColor])\n\n    /**\n     * Repaints everything. Called on mount, on resize, and after undo and clear — anything that\n     * invalidates the pixels rather than adding to them. A stroke in progress does not come\n     * through here; it draws only its newest segment, so a long signature costs the same per\n     * sample as a short one.\n     */\n    const repaint = React.useCallback(() => {\n      const canvas = canvasRef.current\n      const ctx = canvas?.getContext?.(\"2d\")\n      if (!canvas || !ctx) return\n\n      const box = canvas.getBoundingClientRect?.()\n      const cssWidth = box?.width || 0\n      const cssHeight = box?.height || height\n      const backing = backingSize(cssWidth, cssHeight, globalThis.devicePixelRatio ?? 1, maxPixelRatio)\n\n      // Assigning either attribute resets the whole context — transform, colours, and the pixels —\n      // so it is done only when the size really changed. Writing the same number every frame would\n      // otherwise erase the stroke being drawn.\n      if (canvas.width !== backing.width || canvas.height !== backing.height) {\n        canvas.width = backing.width\n        canvas.height = backing.height\n      } else {\n        ctx.clearRect(0, 0, canvas.width, canvas.height)\n      }\n\n      // setTransform, not scale: scale multiplies into whatever transform is already there, so a\n      // pad that survives two resizes ends up drawing at 4x and the signature walks off the box.\n      ctx.setTransform(backing.ratio, 0, 0, backing.ratio, 0, 0)\n      ctx.lineWidth = penWidth\n      ctx.lineCap = \"round\"\n      ctx.lineJoin = \"round\"\n      ctx.strokeStyle = ink\n      ctx.fillStyle = ink\n\n      if (typedName.trim()) {\n        const size = Math.min(cssHeight * 0.42, 64)\n        ctx.font = `${size}px ${SCRIPT_FONT}`\n        ctx.textAlign = \"center\"\n        ctx.textBaseline = \"middle\"\n        ctx.fillText(typedName.trim(), cssWidth / 2, cssHeight / 2, Math.max(cssWidth - 24, 1))\n        return\n      }\n      for (const stroke of strokes) paintStroke(ctx, stroke, penWidth)\n    }, [height, ink, maxPixelRatio, penWidth, strokes, typedName])\n\n    // Repaint whenever the value or the pen changes, and whenever the box is resized. The resize is\n    // observed rather than listened for on the window, because the box also changes width when a\n    // sidebar opens, a tab it lives in becomes visible, or a container query fires — none of which\n    // is a window resize, and all of which would otherwise leave a stretched-looking signature.\n    React.useEffect(() => {\n      repaint()\n      const node = canvasRef.current\n      if (!node || typeof ResizeObserver === \"undefined\") return\n      const observer = new ResizeObserver(() => repaint())\n      observer.observe(node)\n      return () => observer.disconnect()\n    }, [repaint])\n\n    /** Where a pointer event landed, in the box's own CSS pixels. */\n    const pointFrom = (event: { clientX: number; clientY: number }): SignaturePoint => {\n      const box = canvasRef.current?.getBoundingClientRect?.()\n      // clientX minus the box, not offsetX: once the pointer is captured it keeps reporting after\n      // it leaves the element, and offsetX is then measured against whatever it is over instead.\n      return { x: event.clientX - (box?.left ?? 0), y: event.clientY - (box?.top ?? 0) }\n    }\n\n    const handlePointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {\n      if (disabled) return\n      // A right-click or a two-finger press is not a stroke, and a stroke started by one never\n      // gets a matching pointerup.\n      if (event.pointerType === \"mouse\" && event.button !== 0) return\n      // A second finger landing mid-stroke is a palm or a page being pinched, not a second\n      // signature. Without this the stroke in progress is dropped on the floor — overwritten\n      // before it was ever committed, so it vanishes at the next repaint.\n      if (drawing.current) return\n      // Without capture the stroke ends the moment the hand crosses the edge of the box, which is\n      // where the descender of a signature usually goes. With it, the events keep arriving here\n      // until the pointer comes up, wherever that happens to be.\n      event.currentTarget.setPointerCapture?.(event.pointerId)\n      const point = pointFrom(event)\n      drawing.current = { pointerId: event.pointerId, points: [point], from: point }\n      setAnnouncement(\"\")\n    }\n\n    const handlePointerMove = (event: React.PointerEvent<HTMLCanvasElement>) => {\n      const active = drawing.current\n      if (!active || active.pointerId !== event.pointerId) return\n      event.preventDefault()\n\n      // One pointermove is delivered per frame, but the pointer was sampled many times inside that\n      // frame and the browser keeps the ones it skipped. A quick signature is where the difference\n      // shows: at 60Hz a fast flick is four or five points and comes out as a zigzag, while the\n      // coalesced list holds the twenty the digitiser actually saw. Safari has returned an empty\n      // list here, so the event itself is the fallback rather than the assumption.\n      const native = event.nativeEvent\n      const coalesced =\n        typeof native.getCoalescedEvents === \"function\" ? native.getCoalescedEvents() : []\n      const samples = coalesced.length > 0 ? coalesced : [native]\n\n      const ctx = canvasRef.current?.getContext?.(\"2d\")\n      for (const sample of samples) {\n        const point = pointFrom(sample)\n        const previous = active.points[active.points.length - 1]\n        active.points.push(point)\n        if (!ctx) continue\n        // The newest segment only: control at the sample just gone, ending at the midpoint between\n        // it and this one — the same curve `strokeGeometry` produces for the finished stroke.\n        const to = midpoint(previous, point)\n        ctx.beginPath()\n        ctx.moveTo(active.from.x, active.from.y)\n        ctx.quadraticCurveTo(previous.x, previous.y, to.x, to.y)\n        ctx.stroke()\n        active.from = to\n      }\n    }\n\n    const endStroke = (event: React.PointerEvent<HTMLCanvasElement>) => {\n      const active = drawing.current\n      if (!active || active.pointerId !== event.pointerId) return\n      drawing.current = null\n      // Asked first, because releasing a capture that is already gone throws rather than no-oping —\n      // and pointercancel, which is one of the two ways a stroke ends here, has already released it.\n      if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {\n        event.currentTarget.releasePointerCapture(event.pointerId)\n      }\n\n      const box = canvasRef.current?.getBoundingClientRect?.()\n      commit({\n        type: \"drawn\",\n        strokes: [...strokes, active.points],\n        width: box?.width || 0,\n        height: box?.height || height,\n      })\n      setAnnouncement(text.signed)\n    }\n\n    const setStrokes = (next: SignatureStroke[]) => {\n      const box = canvasRef.current?.getBoundingClientRect?.()\n      commit(\n        next.length === 0\n          ? null\n          : { type: \"drawn\", strokes: next, width: box?.width || 0, height: box?.height || height }\n      )\n    }\n\n    const handle: SignaturePadHandle = {\n      clear: () => {\n        commit(null)\n        setAnnouncement(text.cleared)\n      },\n      undo: () => {\n        if (value?.type !== \"drawn\" || value.strokes.length === 0) return\n        setStrokes(value.strokes.slice(0, -1))\n      },\n      isEmpty: () => isSignatureEmpty(value),\n      getValue: () => value ?? null,\n      toSVG: (options) => {\n        const box = canvasRef.current?.getBoundingClientRect?.()\n        return signatureToSvg(value, {\n          width: value?.type === \"drawn\" ? value.width : box?.width || 0,\n          height: value?.type === \"drawn\" ? value.height : box?.height || height,\n          penColor: ink,\n          penWidth,\n          ...options,\n        })\n      },\n      toDataURL: (options) => rasterise(value, { penColor: ink, penWidth, height }, options),\n    }\n    React.useImperativeHandle(ref, () => handle)\n\n    const empty = isSignatureEmpty(value)\n    const svg = React.useMemo(\n      () =>\n        name && value?.type === \"drawn\"\n          ? signatureToSvg(value, {\n              width: value.width,\n              height: value.height,\n              penColor: ink,\n              penWidth,\n            })\n          : null,\n      [ink, name, penWidth, value]\n    )\n\n    return (\n      <div\n        ref={boxRef}\n        role=\"group\"\n        aria-labelledby={label ? labelId : undefined}\n        aria-describedby={hintId}\n        aria-required={required || undefined}\n        className={cn(\"flex w-full flex-col gap-2 text-foreground\", className)}\n        {...props}\n      >\n        {label ? (\n          <span id={labelId} className=\"text-sm font-medium\">\n            {label}\n            {required ? (\n              <span aria-hidden=\"true\" className=\"ml-0.5 text-destructive\">\n                *\n              </span>\n            ) : null}\n          </span>\n        ) : null}\n\n        <div\n          className={cn(\n            \"relative overflow-hidden rounded-md border border-border bg-background\",\n            disabled && \"pointer-events-none opacity-60\",\n            canvasClassName\n          )}\n          style={{ height }}\n        >\n          {/*\n            Hidden from assistive technology on purpose. There is nothing to read inside a canvas,\n            and announcing it as an image or a named region would promise a control that cannot be\n            operated without a pointer. The typed field below is the path that is announced, and\n            the two are described together by the group's label.\n          */}\n          <canvas\n            ref={canvasRef}\n            aria-hidden=\"true\"\n            className={cn(\n              \"h-full w-full touch-none\",\n              // touch-action: none, or the first downward stroke on a phone scrolls the page\n              // instead of drawing — the browser waits to see whether a touch is a gesture, and\n              // the delay eats the beginning of the signature even when it decides it was not.\n              disabled ? \"cursor-not-allowed\" : \"cursor-crosshair\"\n            )}\n            onPointerDown={handlePointerDown}\n            onPointerMove={handlePointerMove}\n            onPointerUp={endStroke}\n            // Capture makes cancel rare, but a system gesture or a palm rejection still fires it,\n            // and a stroke left open would swallow the next one.\n            onPointerCancel={endStroke}\n          />\n          {empty ? (\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-x-0 bottom-3 text-center text-xs text-muted-foreground\"\n            >\n              ✕ — — — — —\n            </span>\n          ) : null}\n        </div>\n\n        <div className=\"flex items-center justify-between gap-2\">\n          <p id={hintId} className=\"text-xs text-muted-foreground\">\n            {hint ?? \"Draw your signature above with a mouse, finger or stylus.\"}\n          </p>\n          <div className=\"flex shrink-0 gap-1\">\n            <button\n              type=\"button\"\n              disabled={disabled || value?.type !== \"drawn\" || strokes.length === 0}\n              onClick={handle.undo}\n              className={BUTTON_CLASS}\n            >\n              {text.undo}\n            </button>\n            <button\n              type=\"button\"\n              disabled={disabled || empty}\n              onClick={handle.clear}\n              className={BUTTON_CLASS}\n            >\n              {text.clear}\n            </button>\n          </div>\n        </div>\n\n        {allowTyped ? (\n          <>\n            <div aria-hidden=\"true\" className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n              <span className=\"h-px flex-1 bg-border\" />\n              {dividerLabel}\n              <span className=\"h-px flex-1 bg-border\" />\n            </div>\n            <div className=\"flex flex-col gap-1\">\n              <label htmlFor={typedId} className=\"text-xs font-medium text-muted-foreground\">\n                {typedLabel}\n              </label>\n              <input\n                id={typedId}\n                type=\"text\"\n                autoComplete=\"name\"\n                disabled={disabled}\n                value={typedName}\n                required={required && empty && !disabled}\n                aria-describedby={hintId}\n                onChange={(event) => {\n                  const next = event.currentTarget.value\n                  // Typing replaces a drawing rather than joining it: there is one signature, and\n                  // a value that held both would leave the consumer to decide which one was meant.\n                  commit(next.trim() ? { type: \"typed\", name: next } : null)\n                  setAnnouncement(next.trim() ? text.signedAs(next.trim()) : text.cleared)\n                }}\n                className=\"h-9 w-full rounded-md border border-border bg-background px-3 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n              />\n            </div>\n          </>\n        ) : null}\n\n        {/*\n          The value a plain form posts. A drawn signature goes as an SVG data URL and a typed one as\n          the name, so a server that only ever receives `signature` gets something it can store\n          either way.\n\n          `required` deliberately does not live here. A hidden input is barred from constraint\n          validation outright — the attribute parses, the browser ignores it, and the form submits\n          unsigned — so it goes on the typed field instead, and only while nothing has been signed:\n          draw, and the requirement lifts without ever having asked for a name. With\n          `allowTyped={false}` there is no field left to carry it, so nothing enforces it natively\n          and the submit handler has to; `aria-required` on the group states the requirement either\n          way.\n        */}\n        {name ? (\n          <input\n            type=\"hidden\"\n            name={name}\n            value={\n              value?.type === \"typed\"\n                ? value.name\n                : svg\n                  ? `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`\n                  : \"\"\n            }\n          />\n        ) : null}\n\n        {/*\n          Announced on the transitions that matter — a stroke landing, a name being typed, the pad\n          being cleared — and never per sample. A live region wired to every pointermove reads\n          numbers over the top of whatever else is being said, which is the failure char-counter\n          documents; the same restraint applies to a surface that emits sixty events a second.\n        */}\n        <span aria-live=\"polite\" className=\"sr-only\">\n          {announcement}\n        </span>\n      </div>\n    )\n  }\n)\n\n/**\n * Rasterises a signature through an offscreen canvas. Returns `null` outside a browser, and when\n * nothing is signed.\n *\n * The default is a transparent background, and that is the one export choice worth stating out\n * loud: a transparent PNG of black ink is invisible the moment it lands on anything dark, which is\n * every dark-mode receipt screen and half of the PDF viewers. Pass `backgroundColor: \"#fff\"` when\n * the image is going somewhere you do not control.\n */\nfunction rasterise(\n  value: SignatureValue | null | undefined,\n  defaults: { penColor: string; penWidth: number; height: number },\n  options: SignatureRasterOptions = {}\n): string | null {\n  if (isSignatureEmpty(value) || !value) return null\n  if (typeof document === \"undefined\") return null\n\n  const {\n    type = \"image/png\",\n    quality,\n    scale = 2,\n    penColor = defaults.penColor,\n    penWidth = defaults.penWidth,\n    backgroundColor,\n    trim = false,\n  } = options\n\n  const width = value.type === \"drawn\" ? value.width : 320\n  const height = value.type === \"drawn\" ? value.height : defaults.height\n  const box = value.type === \"drawn\" && trim !== false ? signatureBounds(value.strokes, penWidth) : null\n  const pad = typeof trim === \"number\" ? trim : TRIM_PADDING\n  const outW = box ? box.width + pad * 2 : width\n  const outH = box ? box.height + pad * 2 : height\n\n  const canvas = document.createElement(\"canvas\")\n  canvas.width = Math.max(1, Math.round(outW * scale))\n  canvas.height = Math.max(1, Math.round(outH * scale))\n  const ctx = canvas.getContext(\"2d\")\n  if (!ctx) return null\n\n  ctx.setTransform(scale, 0, 0, scale, 0, 0)\n  if (backgroundColor) {\n    ctx.fillStyle = backgroundColor\n    ctx.fillRect(0, 0, outW, outH)\n  }\n  if (box) ctx.translate(-(box.x - pad), -(box.y - pad))\n\n  ctx.lineWidth = penWidth\n  ctx.lineCap = \"round\"\n  ctx.lineJoin = \"round\"\n  ctx.strokeStyle = penColor\n  ctx.fillStyle = penColor\n\n  if (value.type === \"typed\") {\n    const size = Math.min(outH * 0.42, 64)\n    ctx.font = `${size}px ${SCRIPT_FONT}`\n    ctx.textAlign = \"center\"\n    ctx.textBaseline = \"middle\"\n    ctx.fillText(value.name.trim(), outW / 2, outH / 2, Math.max(outW - 24, 1))\n  } else {\n    for (const stroke of value.strokes) paintStroke(ctx, stroke, penWidth)\n  }\n  return canvas.toDataURL(type, quality)\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 103 components: https://pulld.pages.dev/?utm_source=cli"
}
