{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "punch-figure",
  "title": "Punch Figure",
  "description": "An issued amount punched through the sheet as a dot matrix by a single left-to-right head pass: subtractive, permanent, and correctable only by a second pass punching VOID diagonally across it.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/punch-figure/component.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// PunchFigure — an issued amount rendered as a checkwriter dot-matrix punch,\n// not printed text. One governing scalar, the print head's column position,\n// drives everything: which cells are cut through to reveal --background,\n// the sheet's 1px translateY flinch on every step, and how much carriage\n// travel remains. The head crosses the figure exactly once, left to right,\n// at a firm 60ms/column — never faster (flinch would blur into vibration),\n// never slower (it would read as labouring). There is no fade-in and no\n// type-on: typing implies backspace, and the whole point of a punch is that\n// material is REMOVED and cannot be put back. A committed figure can only\n// ever be corrected by a second punched pass — a VOID stamp punched\n// diagonally across it — which adds holes, never closes one.\n//\n// Real amount text lives in the accessible tree at all times (it does not\n// wait on the animation); the dot grid is a decorative, aria-hidden replay\n// of that same value. Issuing fires exactly one polite live-region\n// announcement on completion. There is no interaction inside the mechanism\n// itself — Issue and the confirm-gated Void are real buttons below it.\n// prefers-reduced-motion renders the destination state in one paint, no\n// column-by-column reveal and no flinch.\n// ---------------------------------------------------------------------------\n\nexport interface PunchFigureProps {\n  /** dollar amount to punch, e.g. 1180 -> \"$1,180.00\" */\n  amount?: number;\n  /** currency glyph prefixed onto the punched figure */\n  currency?: string;\n  /** shown in the header and folded into the issue/void announcements */\n  invoiceId?: string;\n  /** small caption above the figure, e.g. \"Invoice\", \"Payout\", \"Credit note\" */\n  label?: string;\n  /** called once the amount finishes punching through (or immediately, under reduced motion) */\n  onIssue?: () => void;\n  /** called once the VOID overpunch finishes (or immediately, under reduced motion) */\n  onVoid?: () => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\n// 5x7 dot-matrix font. Rows top-to-bottom, '#' = punched, '.' = paper.\n// Only the glyphs a checkwriter amount (and a VOID overpunch) ever needs.\nconst FONT: Record<string, string[]> = {\n  \"0\": [\".###.\", \"#...#\", \"#..##\", \"#.#.#\", \"##..#\", \"#...#\", \".###.\"],\n  \"1\": [\"..#..\", \".##..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \".###.\"],\n  \"2\": [\".###.\", \"#...#\", \"....#\", \"...#.\", \"..#..\", \".#...\", \"#####\"],\n  \"3\": [\".###.\", \"#...#\", \"....#\", \"..##.\", \"....#\", \"#...#\", \".###.\"],\n  \"4\": [\"...#.\", \"..##.\", \".#.#.\", \"#..#.\", \"#####\", \"...#.\", \"...#.\"],\n  \"5\": [\"#####\", \"#....\", \"####.\", \"....#\", \"....#\", \"#...#\", \".###.\"],\n  \"6\": [\"..##.\", \".#...\", \"#....\", \"####.\", \"#...#\", \"#...#\", \".###.\"],\n  \"7\": [\"#####\", \"....#\", \"...#.\", \"..#..\", \".#...\", \".#...\", \".#...\"],\n  \"8\": [\".###.\", \"#...#\", \"#...#\", \".###.\", \"#...#\", \"#...#\", \".###.\"],\n  \"9\": [\".###.\", \"#...#\", \"#...#\", \".####\", \"....#\", \"...#.\", \".##..\"],\n  $: [\"..#..\", \".####\", \"#.#..\", \".###.\", \"..#.#\", \"####.\", \"..#..\"],\n  \",\": [\".....\", \".....\", \".....\", \".....\", \"..##.\", \"..#..\", \".#...\"],\n  \".\": [\".....\", \".....\", \".....\", \".....\", \".....\", \".##..\", \".##..\"],\n  V: [\"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \".#.#.\", \"..#..\"],\n  O: [\".###.\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \".###.\"],\n  I: [\"#####\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \"#####\"],\n  D: [\"####.\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"####.\"],\n};\nconst BLANK = [\".....\", \".....\", \".....\", \".....\", \".....\", \".....\", \".....\"];\n\nconst STEP_MS = 60; // firm per-column cadence — see file header\nconst ARM_MS = 4000; // Void confirm-arm window before it disarms itself\nconst CELL = 5;\nconst GAP = 1.1;\nconst VOID_CELL = 7;\nconst VOID_GAP = 2;\n\ninterface ColumnLayout {\n  /** rows[r][c] === true means glyph column c, row r is a punchable dot */\n  rows: boolean[][];\n  cols: number;\n}\n\nfunction buildColumns(text: string): ColumnLayout {\n  const rows: boolean[][] = [[], [], [], [], [], [], []];\n  const chars = [...text];\n  chars.forEach((ch, i) => {\n    const glyph = FONT[ch] ?? BLANK;\n    for (let r = 0; r < 7; r++) {\n      for (let c = 0; c < 5; c++) rows[r].push(glyph[r][c] === \"#\");\n    }\n    if (i < chars.length - 1) {\n      for (let r = 0; r < 7; r++) rows[r].push(false); // one-column gap\n    }\n  });\n  return { rows, cols: rows[0]?.length ?? 0 };\n}\n\nfunction formatAmount(amount: number, currency: string): string {\n  const fixed = Math.abs(amount).toFixed(2);\n  const [whole, cents] = fixed.split(\".\");\n  const grouped = whole.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n  return `${amount < 0 ? \"-\" : \"\"}${currency}${grouped}.${cents}`;\n}\n\ntype Phase = \"unissued\" | \"issuing\" | \"issued\" | \"voiding\" | \"voided\";\n\nfunction PunchGrid({\n  layout,\n  headCol,\n  cell,\n  gap,\n}: {\n  layout: ColumnLayout;\n  headCol: number;\n  cell: number;\n  gap: number;\n}) {\n  const holes: React.ReactNode[] = [];\n  const lastCol = Math.min(headCol, layout.cols - 1);\n  for (let r = 0; r < 7; r++) {\n    const row = layout.rows[r];\n    for (let c = 0; c <= lastCol; c++) {\n      if (row[c]) {\n        holes.push(\n          <span\n            key={`${r}-${c}`}\n            className=\"ns-pf-hole\"\n            style={{ gridColumn: c + 1, gridRow: r + 1 }}\n          />\n        );\n      }\n    }\n  }\n  return (\n    <div\n      className=\"ns-pf-grid\"\n      style={{\n        gridTemplateColumns: `repeat(${layout.cols}, ${cell}px)`,\n        gridTemplateRows: `repeat(7, ${cell}px)`,\n        gap: `${gap}px`,\n        width: layout.cols * cell + Math.max(0, layout.cols - 1) * gap,\n        height: 7 * cell + 6 * gap,\n      }}\n    >\n      {holes}\n    </div>\n  );\n}\n\nexport function PunchFigure({\n  amount = 1180,\n  currency = \"$\",\n  invoiceId = \"INV-2041\",\n  label = \"Invoice\",\n  onIssue,\n  onVoid,\n  className = \"\",\n}: PunchFigureProps) {\n  const autoId = useId().replace(/:/g, \"\");\n  const sheetRef = useRef<HTMLDivElement | null>(null);\n  const reducedRef = useRef(false);\n  const armTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const onIssueRef = useRef(onIssue);\n  onIssueRef.current = onIssue;\n  const onVoidRef = useRef(onVoid);\n  onVoidRef.current = onVoid;\n\n  const [phase, setPhase] = useState<Phase>(\"unissued\");\n  const [headCol, setHeadCol] = useState(-1);\n  const [voidHeadCol, setVoidHeadCol] = useState(-1);\n  const [voidArmed, setVoidArmed] = useState(false);\n  const [announce, setAnnounce] = useState(\"\");\n\n  const display = useMemo(() => formatAmount(amount, currency), [amount, currency]);\n  const layout = useMemo(() => buildColumns(display), [display]);\n  const voidLayout = useMemo(() => buildColumns(\"VOID\"), []);\n\n  // Reduced motion is decided before the browser ever paints: a\n  // useLayoutEffect (not useEffect) so a reduced-motion visitor never sees\n  // one frame of blank sheet before it jumps to fully punched — \"at first\n  // paint\" is a layout-timing requirement, not just \"skip the animation.\"\n  useLayoutEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    reducedRef.current = mq.matches;\n    if (mq.matches) {\n      setHeadCol(layout.cols - 1);\n      setPhase(\"issued\");\n    }\n    const onChange = () => {\n      reducedRef.current = mq.matches;\n    };\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => () => clearTimeout(armTimeoutRef.current), []);\n\n  const flinch = useCallback(() => {\n    const el = sheetRef.current;\n    if (!el || reducedRef.current) return;\n    el.classList.remove(\"ns-pf-flinch\");\n    void el.offsetWidth; // restart the CSS animation\n    el.classList.add(\"ns-pf-flinch\");\n  }, []);\n\n  // -- issuing pass: the head crosses the amount exactly once ---------------\n  useEffect(() => {\n    if (phase !== \"issuing\") return;\n    let col = -1;\n    const id = setInterval(() => {\n      col += 1;\n      setHeadCol(col);\n      flinch();\n      if (col >= layout.cols - 1) {\n        clearInterval(id);\n        setPhase(\"issued\");\n        setAnnounce(`Invoice ${invoiceId} issued for ${display}.`);\n        onIssueRef.current?.();\n      }\n    }, STEP_MS);\n    return () => clearInterval(id);\n  }, [phase, layout.cols, flinch, invoiceId, display]);\n\n  // -- void pass: a second head punches VOID on the diagonal, over the top --\n  useEffect(() => {\n    if (phase !== \"voiding\") return;\n    let col = -1;\n    const id = setInterval(() => {\n      col += 1;\n      setVoidHeadCol(col);\n      flinch();\n      if (col >= voidLayout.cols - 1) {\n        clearInterval(id);\n        setPhase(\"voided\");\n        setAnnounce(`Invoice ${invoiceId} voided.`);\n        onVoidRef.current?.();\n      }\n    }, STEP_MS);\n    return () => clearInterval(id);\n  }, [phase, voidLayout.cols, flinch, invoiceId]);\n\n  const handleIssue = useCallback(() => {\n    if (phase !== \"unissued\") return;\n    if (reducedRef.current) {\n      setHeadCol(layout.cols - 1);\n      setPhase(\"issued\");\n      setAnnounce(`Invoice ${invoiceId} issued for ${display}.`);\n      onIssueRef.current?.();\n      return;\n    }\n    setPhase(\"issuing\");\n  }, [phase, layout.cols, invoiceId, display]);\n\n  const disarmVoid = useCallback(() => {\n    clearTimeout(armTimeoutRef.current);\n    setVoidArmed(false);\n  }, []);\n\n  const handleVoid = useCallback(() => {\n    if (phase !== \"issued\") return;\n    if (!voidArmed) {\n      setVoidArmed(true);\n      armTimeoutRef.current = setTimeout(() => setVoidArmed(false), ARM_MS);\n      return;\n    }\n    disarmVoid();\n    if (reducedRef.current) {\n      setVoidHeadCol(voidLayout.cols - 1);\n      setPhase(\"voided\");\n      setAnnounce(`Invoice ${invoiceId} voided.`);\n      onVoidRef.current?.();\n      return;\n    }\n    setPhase(\"voiding\");\n  }, [phase, voidArmed, disarmVoid, voidLayout.cols, invoiceId]);\n\n  const handleVoidKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (e.key === \"Escape\" && voidArmed) {\n        e.preventDefault();\n        disarmVoid();\n      }\n    },\n    [voidArmed, disarmVoid]\n  );\n\n  const travel = layout.cols > 1 ? Math.max(0, Math.min(1, (headCol + 1) / layout.cols)) : 0;\n\n  const statusText: Record<Phase, string> = {\n    unissued: \"Not yet issued\",\n    issuing: \"Issuing…\",\n    issued: \"Issued\",\n    voiding: \"Voiding…\",\n    voided: \"Void\",\n  };\n\n  const srSuffix =\n    phase === \"voided\" ? \" — VOID, does not settle.\" : phase === \"unissued\" || phase === \"issuing\" ? \" (draft, not yet issued)\" : \"\";\n\n  return (\n    <div className={`ns-pf-root w-full max-w-sm ${className}`}>\n      <style>{CSS}</style>\n\n      <span role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {announce}\n      </span>\n\n      <div\n        ref={sheetRef}\n        className={`ns-pf-sheet relative overflow-hidden rounded-[16px] border border-border p-5 ${\n          phase !== \"unissued\" ? \"ns-pf-active\" : \"\"\n        }`}\n        aria-hidden=\"true\"\n      >\n        <div className=\"flex items-baseline justify-between gap-3\">\n          <p className=\"font-mono text-[10px] uppercase tracking-[0.22em] text-ns-muted\">{label}</p>\n          <p className=\"font-mono text-[10px] uppercase tracking-[0.15em] text-ns-muted\">{invoiceId}</p>\n        </div>\n\n        <div className=\"ns-pf-mech relative mt-4\">\n          <PunchGrid layout={layout} headCol={headCol} cell={CELL} gap={GAP} />\n          {(phase === \"voiding\" || phase === \"voided\") && (\n            <div\n              className=\"ns-pf-void-layer absolute left-1/2 top-1/2\"\n              style={{\n                transform: `translate(-50%, -50%) rotate(-14deg)`,\n              }}\n            >\n              <PunchGrid layout={voidLayout} headCol={voidHeadCol} cell={VOID_CELL} gap={VOID_GAP} />\n            </div>\n          )}\n        </div>\n\n        <div className=\"ns-pf-track mt-4\">\n          <div className=\"ns-pf-track-fill\" style={{ width: `${travel * 100}%` }} />\n          <div className=\"ns-pf-track-head\" style={{ left: `${travel * 100}%` }} />\n        </div>\n      </div>\n\n      <p id={`pf-amount-${autoId}`} className=\"sr-only\">\n        {label} {invoiceId}: {display}\n        {srSuffix}\n      </p>\n\n      <div className=\"mt-3 flex items-center justify-between gap-3\">\n        <span className=\"ns-pf-status-badge font-mono text-[11px] uppercase tracking-[0.12em] text-ns-muted\">\n          {statusText[phase]}\n        </span>\n        <div className=\"flex items-center gap-2\">\n          <button\n            type=\"button\"\n            data-pf-issue\n            onClick={handleIssue}\n            disabled={phase !== \"unissued\"}\n            aria-describedby={`pf-amount-${autoId}`}\n            className=\"ns-pf-btn\"\n          >\n            {phase === \"unissued\" ? \"Issue\" : phase === \"issuing\" ? \"Issuing…\" : \"Issued\"}\n          </button>\n          <button\n            type=\"button\"\n            data-pf-void\n            onClick={handleVoid}\n            onKeyDown={handleVoidKeyDown}\n            onBlur={() => voidArmed && disarmVoid()}\n            disabled={phase !== \"issued\"}\n            className={`ns-pf-btn ${voidArmed ? \"ns-pf-btn-armed\" : \"\"}`}\n          >\n            {phase === \"voided\" ? \"Voided\" : phase === \"voiding\" ? \"Voiding…\" : voidArmed ? \"Confirm void\" : \"Void\"}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nconst CSS = `\n/* A hole is var(--background) showing through the sheet, so the sheet tint IS\n   the contrast that makes the punched figure readable. At the original 4% the\n   paper sat 4% off the hole colour and the whole figure rode on the 1px ring\n   below — which, on a ~5px dot in --border, rendered the amount essentially\n   invisible in the light theme (verified in light-open.png). 10% gives the\n   paper a real edge against the void in both themes without turning the sheet\n   into a filled block. */\n.ns-pf-sheet{ background: color-mix(in oklab, var(--foreground) 10%, var(--background)); }\n.ns-pf-grid{ position: relative; display: grid; }\n.ns-pf-hole{\n  background: var(--background);\n  border-radius: 9999px;\n  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--foreground) 35%, transparent);\n}\n.ns-pf-void-layer{ opacity: 0.92; }\n@keyframes ns-pf-flinch-kf{\n  0%{ transform: translateY(0); }\n  35%{ transform: translateY(1px); }\n  70%{ transform: translateY(-0.3px); }\n  100%{ transform: translateY(0); }\n}\n.ns-pf-flinch{ animation: ns-pf-flinch-kf 90ms ease-out; }\n\n.ns-pf-track{ position: relative; height: 2px; border-radius: 9999px; background: color-mix(in oklab, var(--ns-muted) 30%, transparent); }\n.ns-pf-track-fill{ position: absolute; inset: 0 auto 0 0; height: 100%; border-radius: 9999px; background: var(--border); transition: width 60ms linear; }\n.ns-pf-track-head{ position: absolute; top: 50%; width: 3px; height: 8px; margin-left: -1.5px; border-radius: 1px; background: var(--foreground); transform: translateY(-50%); transition: left 60ms linear; }\n\n.ns-pf-btn{\n  border-radius: 9999px;\n  border: 1px solid var(--border);\n  background: var(--background);\n  color: var(--foreground);\n  padding: 0.4rem 0.9rem;\n  font-size: 0.75rem;\n  font-weight: 500;\n  transition: border-color 150ms ease-out, color 150ms ease-out;\n}\n.ns-pf-btn:hover:not(:disabled){ border-color: var(--foreground); }\n.ns-pf-btn:disabled{ color: var(--ns-muted); cursor: not-allowed; }\n.ns-pf-btn:focus-visible{ outline: 2px solid var(--ns-accent); outline-offset: 2px; }\n.ns-pf-btn-armed{ border-color: var(--ns-accent); color: var(--ns-accent); }\n\n@media (prefers-reduced-motion: reduce){\n  .ns-pf-flinch{ animation: none !important; }\n  .ns-pf-track-fill, .ns-pf-track-head{ transition: none !important; }\n  .ns-pf-btn{ transition: none !important; }\n}\n`;\n\nexport default PunchFigure;\n",
      "type": "registry:ui",
      "target": "components/ui/punch-figure.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "invoice",
      "payout",
      "amount",
      "checkwriter",
      "dot-matrix",
      "immutable",
      "micro-interaction",
      "accessibility"
    ],
    "instruction": "Render an issued amount (default $1,180.00, framed exactly as a checkwriter prints it) as a DOM dot-matrix punch card, never as flat text — the visible figure is a grid of 5x7-dot glyphs where a 'hole' is a cell whose background is set to var(--background) so it reads as cut clean through the sheet (the sheet itself is a slightly tinted card, `color-mix(in oklab, var(--foreground) 4%, var(--background))`, so a punched hole visibly differs from unpunched paper); unpunched cells render nothing at all, not a ghost/guide dot, because the card is physically blank there until the head arrives — never a fade-in or type-on, since typing implies backspace and the whole payload here is that a punched column cannot be un-punched.\n\nOne governing scalar, the print head's integer column position (headCol), derives everything: which columns are eligible to show their punched dots (columns after headCol render nothing, columns at or before it show whichever cells their glyph's bitmap marks), the sheet's per-column flinch (a 1px translateY dip with a slight spring-back overshoot, ~90ms, restarted via a classList remove/reflow/add trick on every column tick — never a React re-render of the whole sheet), and the 'carriage travel' strip (a thin 2px track under the figure with a fill and a small head marker whose width/position are `(headCol+1)/totalColumns`). The head steps at a firm 60ms per column via `setInterval` — faster and the flinch frames merge into vibration, slower reads as labouring; both are named failure modes, don't retune this without cause. Columns include a 1-column blank gap between glyphs, and the head steps through gap columns too (no flinch skip), giving the pass a slight breath between characters. Critically: one head, one pass, strictly left to right — column i's dots can never become visible before column i-1's, so drive headCol from a single incrementing interval, never compute it from multiple independent effects that could race.\n\nThe issuing sequence starts only from an explicit 'Issue' button click (real `<button>`, `data-pf-issue`, outside the punched grid — there is no interaction inside the mechanism itself). Before issuing, the button reads 'Issue'; mid-pass it reads 'Issuing…' and is disabled; once the head reaches the last column it reads 'Issued' and stays permanently disabled — an issued figure cannot be re-issued or edited, only voided. On completion, fire exactly one `role=status aria-live=polite` announcement: 'Invoice <id> issued for <amount>.' — not one announcement per column.\n\nCorrection is a second, independent punched pass: a 'Void' button (also outside the mechanism, disabled until issued) is confirm-gated — first click arms it (label becomes 'Confirm void', a `var(--ns-accent)` border cues the armed state, and it auto-disarms after 4s or on Escape or on blur); the second click while armed starts the void pass. That pass punches the literal word 'VOID' as its own 5x7-per-letter dot grid, rendered as a second absolutely-positioned layer centered over the amount grid and rotated -14deg, using a larger cell pitch than the amount grid so it reads as a stamp crossing it diagonally. It runs the exact same one-head/60ms-per-column mechanism as the issuing pass, on its own headCol, and shares the same sheet flinch. Voiding adds holes; it never removes or resets any hole already punched into the amount — the original figure stays fully intact underneath the VOID overpunch, because a void doesn't erase the record, it marks it dead. Once the void pass completes, both buttons are permanently disabled ('Issued' / 'Voided'), fire one more polite announcement ('Invoice <id> voided.'), and nothing in the component can change state again.\n\nAccessibility: the entire punched-grid mechanism (both layers, the flinch, the carriage track) is `aria-hidden`. The real amount always exists as plain text in the accessible tree via a `sr-only` node — `<label> <id>: <amount>` plus a short draft/void suffix reflecting current phase — present from first paint regardless of whether the head has punched anything yet, so a screen-reader user is never waiting on an animation for the number. A small plain-text status badge (Not yet issued / Issuing… / Issued / Voiding… / Void) sits next to the buttons for sighted users who don't want to parse the dot grid.\n\nColors: `--background --foreground --ns-muted --border --ns-accent` only, plus `color-mix()` built from those tokens for the sheet tint and track fill — no hex, no `rgb()/hsl()`, no Tailwind palette classes. `--ns-accent` appears exactly once, as the armed-Void border/text cue, and nowhere else — it is not used for the punch holes or the sheet itself. Radii: 16px on the outer sheet, full (9999px) on the punched holes, the carriage-track pieces, and the pill-shaped Issue/Void buttons. `prefers-reduced-motion: reduce` skips the per-column interval entirely on both passes: clicking Issue (or Void, once armed and confirmed) jumps straight from -1 to the final column in one paint, the flinch keyframes and track transitions are suppressed via `!important`, but the button/gate flow and the one-time announcements are unchanged — reduced motion is still fully usable, just without the physical theater. Pure DOM + CSS, no `<canvas>`/WebGL. Distinct from card-number-emboss: that component is additive (typed digits raise as embossed metal while the user is still mid-entry, and can be edited freely up to blur-validation) — this one is subtractive (material is punched OUT of the sheet in a single committed pass, and once punched the figure is permanently read-only, correctable only by overpunching VOID, never by editing)."
  },
  "type": "registry:ui"
}