{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confirm-hold-wax",
  "title": "Confirm Hold Wax",
  "description": "Press-and-hold confirm as a molten wax seal — holding pours a wobbling gooey blob of deep crimson wax onto the document line, hold-complete drops a signet stamp that squashes it into a scalloped, monogrammed seal that cools, darkens and micro-cracks over 2s, while early release slumps the blob and drains it back.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/confirm-hold-wax/component.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// SignetDrop — press-and-hold confirm rendered as a molten wax seal, not a\n// progress bar. Holding the \"Seal\" button pours molten crimson wax onto a\n// document line beneath: an SVG gooey filter (feGaussianBlur -> feColorMatrix\n// alpha-threshold) fuses three growing circles into one liquid blob that\n// wobbles on sin/cos jitter while it is still molten. Reaching the hold\n// threshold triggers the signet: a stamp drops fast from above (ease-in),\n// squash-and-rebounds on impact, presses the blob into a static scalloped\n// seal disc with an embossed monogram (two offset copies faking a bevel) and\n// a squish ring of displaced wax puffing outward, then retracts. The seal\n// then COOLS for exactly 2s — the specular sheen dies, the fill interpolates\n// from hot to deep crimson, and three hairline micro-cracks etch in on\n// staggered stroke-dashoffset reveals measured in RAW SVG user units (no\n// pathLength normalization, no vector-effect=\"non-scaling-stroke\" — the two\n// must never be combined). Early release before the threshold is the cancel\n// path: the blob slumps, flattening against the line and draining away, and\n// the button re-arms. Once sealed, state is terminal: the disc stays, the\n// button dims under aria-disabled with its accessible name intact, and a\n// visible \"Sealed\" caption appears — no reset, no replay.\n//\n// Hot path is refs + rAF only: hold progress, wobble jitter, stamp drop,\n// puff ring, cooling color lerp and crack reveals are all direct\n// setAttribute writes on SVG refs. React state carries only the discrete\n// phase and the sr-only announcement text.\n//\n// Accessibility: the hold works from the keyboard (Space/Enter down starts,\n// up before threshold cancels); a dedicated sr-only span\n// (role=status/aria-live=polite/aria-atomic=true) announces \"Poured\",\n// \"Sealed\" and \"Cancelled\", with a zero-width-space parity toggle so a\n// repeated identical message (two cancels in a row) still re-announces.\n// prefers-reduced-motion keeps the hold requirement (intent is preserved)\n// but skips every intermediate visual — the moment progress reaches 1.0 the\n// fully cooled, cracked seal appears in a single discrete step.\n// ---------------------------------------------------------------------------\n\nexport type SignetDropPhase =\n  | \"idle\"\n  | \"pouring\"\n  | \"stamping\"\n  | \"cooling\"\n  | \"sealed\"\n  | \"cancelling\";\n\nexport interface SignetDropProps {\n  /** Hold duration required to complete the seal, in ms. Default 1100. */\n  holdMs?: number;\n  /**\n   * Self-driving mode for automated previews: runs a scripted timeline\n   * through the same internal hold/release functions the real pointer and\n   * keyboard handlers use — two short early-release holds (slump) in the\n   * first seconds, then one full hold that seals permanently. Scripted steps\n   * yield to any real interaction in progress.\n   */\n  demo?: boolean;\n  /** Called once when the terminal sealed state is reached. */\n  onSealed?: () => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\ntype HoldSource = \"real\" | \"synthetic\";\n\nconst HOLD_DEFAULT_MS = 1100;\nconst CANCEL_MS = 520;\nconst DROP_MS = 190;\nconst SQUASH_MS = 260;\nconst RETRACT_MS = 240;\nconst PUFF_MS = 380;\nconst COOL_MS = 2000;\nconst CRACK_STAGGER_MS = 420;\nconst CRACK_MS = 700;\n\n// Stage geometry (SVG user units, viewBox 0 0 280 170).\nconst CX = 140;\nconst CY = 112;\nconst SEAL_R = 29;\nconst LINE_Y = 120;\nconst STAMP_ANCHOR_Y = 86;\nconst STAMP_RAISE = -92;\n\n// Deep crimson wax — component-local by design; the repo has no crimson\n// token and the loud collection is color-exempt. Chrome around the wax\n// stays on the standard tokens.\nconst WAX_MOLTEN = \"#b31b30\";\nconst WAX_HOT = \"#9c1526\";\nconst WAX_COOL = \"#570d18\";\nconst WAX_HIGHLIGHT = \"#d96475\";\nconst WAX_SHADOW = \"#33060d\";\n\nconst GOO_BLOBS = [\n  { x: 140, y: 110, r: 22 },\n  { x: 127, y: 116, r: 14 },\n  { x: 153, y: 115, r: 13 },\n] as const;\n\nconst PUFF_COUNT = 8;\nconst PUFFS = Array.from({ length: PUFF_COUNT }, (_, i) => {\n  const a = (i / PUFF_COUNT) * Math.PI * 2 + 0.35;\n  return { cos: Math.cos(a), sin: Math.sin(a) };\n});\n\n// Scalloped seal perimeter: a circle whose radius is modulated by a\n// low-frequency cosine — 10 scallops around the rim.\nfunction scallopPath(\n  cx: number,\n  cy: number,\n  r: number,\n  scallops: number,\n  amp: number\n): string {\n  const steps = 140;\n  const parts: string[] = [];\n  for (let i = 0; i <= steps; i++) {\n    const th = (i / steps) * Math.PI * 2;\n    const rr = r + amp * Math.cos(th * scallops);\n    parts.push(\n      `${i === 0 ? \"M\" : \"L\"} ${(cx + rr * Math.cos(th)).toFixed(2)} ${(cy + rr * Math.sin(th)).toFixed(2)}`\n    );\n  }\n  return parts.join(\" \") + \" Z\";\n}\n\nconst SEAL_PATH = scallopPath(CX, CY, SEAL_R, 10, 2.6);\nconst SEAL_RIM = scallopPath(CX, CY, SEAL_R - 4.5, 10, 1.6);\n\n// Hairline micro-cracks, authored directly in raw user units inside the\n// disc (center 140,112, r 29). Revealed via stroke-dasharray/dashoffset\n// measured with getTotalLength() — never pathLength-normalized.\nconst CRACK_PATHS = [\n  \"M 120 102 l 9 4 l 6 -3 l 10 5 l 7 -2\",\n  \"M 158 126 l -8 -3 l -5 4 l -9 -2\",\n  \"M 133 92 l 4 7 l 6 2 l 3 6\",\n] as const;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nconst [HOT_R, HOT_G, HOT_B] = hexToRgb(WAX_HOT);\nconst [COOL_R, COOL_G, COOL_B] = hexToRgb(WAX_COOL);\n\nfunction lerpWax(u: number): string {\n  const r = Math.round(HOT_R + (COOL_R - HOT_R) * u);\n  const g = Math.round(HOT_G + (COOL_G - HOT_G) * u);\n  const b = Math.round(HOT_B + (COOL_B - HOT_B) * u);\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\nconst easeOutCubic = (t: number) => 1 - (1 - t) ** 3;\nconst easeInCubic = (t: number) => t * t * t;\n\n// Squash-and-rebound: instant compression to `deep` at impact, overshoot to\n// `over`, settle at 1.\nfunction squashScale(u: number, deep: number, over: number): number {\n  if (u >= 1) return 1;\n  if (u < 0.5) return deep + (over - deep) * easeOutCubic(u / 0.5);\n  return over + (1 - over) * easeOutCubic((u - 0.5) / 0.5);\n}\n\nconst anchoredScale = (cx: number, cy: number, sx: number, sy: number) =>\n  `translate(${cx} ${cy}) scale(${sx.toFixed(4)} ${sy.toFixed(4)}) translate(${-cx} ${-cy})`;\n\nexport function SignetDrop({\n  holdMs = HOLD_DEFAULT_MS,\n  demo = false,\n  onSealed,\n  className = \"\",\n}: SignetDropProps) {\n  const rawId = useId();\n  const gooId = `signet-goo-${rawId.replace(/:/g, \"\")}`;\n\n  const btnRef = useRef<HTMLButtonElement | null>(null);\n  const gooGroupRef = useRef<SVGGElement | null>(null);\n  const gooRefs = useRef<(SVGCircleElement | null)[]>([]);\n  const sealGroupRef = useRef<SVGGElement | null>(null);\n  const discRef = useRef<SVGPathElement | null>(null);\n  const sheenRef = useRef<SVGEllipseElement | null>(null);\n  const crackRefs = useRef<(SVGPathElement | null)[]>([]);\n  const puffRefs = useRef<(SVGCircleElement | null)[]>([]);\n  const stampOuterRef = useRef<SVGGElement | null>(null);\n  const stampSquashRef = useRef<SVGGElement | null>(null);\n\n  const [phase, setPhase] = useState<SignetDropPhase>(\"idle\");\n  const [announceText, setAnnounceText] = useState(\"\");\n  const parityRef = useRef(false);\n\n  const holdMsRef = useRef(holdMs);\n  holdMsRef.current = holdMs;\n  const onSealedRef = useRef(onSealed);\n  onSealedRef.current = onSealed;\n\n  const stateRef = useRef({\n    phase: \"idle\" as SignetDropPhase,\n    holdSource: null as HoldSource | null,\n    progress: 0,\n    t: 0, // wobble clock (ms)\n    last: 0,\n    raf: 0,\n    reduced: false,\n    stampStart: 0,\n    impactDone: false,\n    coolStart: 0,\n    cancelStart: 0,\n    cancelP: 0,\n    crackLens: [0, 0, 0] as number[],\n  });\n\n  const apiRef = useRef<{\n    start: () => void;\n    release: () => void;\n  } | null>(null);\n\n  const announce = useCallback((message: \"Poured\" | \"Sealed\" | \"Cancelled\") => {\n    // Zero-width-space parity toggle: forces a real text-node change even\n    // when the same message repeats (two \"Cancelled\" in a row), which is\n    // what actually re-triggers an aria-live announcement.\n    parityRef.current = !parityRef.current;\n    setAnnounceText(message + (parityRef.current ? \"​\" : \"\"));\n  }, []);\n\n  useEffect(() => {\n    const s = stateRef.current;\n    const btn = btnRef.current;\n    if (!btn) return;\n\n    const setPhaseBoth = (p: SignetDropPhase) => {\n      s.phase = p;\n      setPhase(p);\n    };\n\n    const gooCircle = (i: number) => gooRefs.current[i] ?? null;\n\n    const hideGoo = () => {\n      const g = gooGroupRef.current;\n      if (g) {\n        g.setAttribute(\"opacity\", \"0\");\n        g.removeAttribute(\"transform\");\n      }\n      GOO_BLOBS.forEach((b, i) => {\n        const c = gooCircle(i);\n        if (!c) return;\n        c.setAttribute(\"r\", \"0\");\n        c.setAttribute(\"cx\", String(b.x));\n        c.setAttribute(\"cy\", String(b.y));\n      });\n    };\n\n    const hidePuffs = () => {\n      for (const p of puffRefs.current) {\n        if (p) p.setAttribute(\"opacity\", \"0\");\n      }\n    };\n\n    const writeGooFrame = (p: number) => {\n      const grow = easeOutCubic(p);\n      GOO_BLOBS.forEach((b, i) => {\n        const c = gooCircle(i);\n        if (!c) return;\n        const wob = 2.4 * p;\n        c.setAttribute(\"r\", (b.r * grow).toFixed(2));\n        c.setAttribute(\n          \"cx\",\n          (b.x + Math.sin(s.t * 0.006 + i * 2.1) * wob).toFixed(2)\n        );\n        c.setAttribute(\n          \"cy\",\n          (b.y + Math.cos(s.t * 0.0048 + i * 1.4) * wob * 0.7).toFixed(2)\n        );\n      });\n    };\n\n    const revealCracksFully = () => {\n      crackRefs.current.forEach((c) => {\n        if (c) c.setAttribute(\"stroke-dashoffset\", \"0\");\n      });\n    };\n\n    const finalizeSealed = () => {\n      const disc = discRef.current;\n      if (disc) disc.setAttribute(\"fill\", WAX_COOL);\n      if (sheenRef.current) sheenRef.current.setAttribute(\"opacity\", \"0\");\n      revealCracksFully();\n      sealGroupRef.current?.removeAttribute(\"transform\");\n      hidePuffs();\n      setPhaseBoth(\"sealed\");\n      announce(\"Sealed\");\n      onSealedRef.current?.();\n    };\n\n    // Reduced motion: the hold was still required, but every intermediate\n    // visual is skipped — one discrete jump to the cooled, cracked seal.\n    const sealInstant = () => {\n      hideGoo();\n      const stamp = stampOuterRef.current;\n      if (stamp) stamp.setAttribute(\"opacity\", \"0\");\n      sealGroupRef.current?.setAttribute(\"opacity\", \"1\");\n      finalizeSealed();\n    };\n\n    const impact = () => {\n      s.impactDone = true;\n      hideGoo();\n      sealGroupRef.current?.setAttribute(\"opacity\", \"1\");\n      discRef.current?.setAttribute(\"fill\", WAX_HOT);\n      sheenRef.current?.setAttribute(\"opacity\", \"0.4\");\n    };\n\n    const beginStamp = (now: number) => {\n      s.holdSource = null; // completion is committed; release can no longer cancel\n      if (s.reduced) {\n        sealInstant();\n        return;\n      }\n      setPhaseBoth(\"stamping\");\n      s.stampStart = now;\n      s.impactDone = false;\n      stampOuterRef.current?.setAttribute(\"opacity\", \"1\");\n    };\n\n    const tick = (now: number) => {\n      // Clamped to 0: a rAF timestamp can read marginally behind the\n      // performance.now() sample wake() took to seed s.last (first frame\n      // after (re)starting the loop), which without the clamp drives\n      // progress slightly negative and easeOutCubic negative in turn —\n      // a negative wax-blob radius the browser rejects outright.\n      const raw = Math.max(0, now - s.last); // wall time keeps the hold duration honest\n      const dt = Math.min(64, raw); // clamped for the wobble clock\n      s.last = now;\n      s.t += dt;\n\n      if (s.phase === \"pouring\") {\n        s.progress = Math.min(1, s.progress + raw / holdMsRef.current);\n        if (!s.reduced) writeGooFrame(s.progress);\n        if (s.progress >= 1) beginStamp(now);\n      } else if (s.phase === \"cancelling\") {\n        const k = Math.min(1, (now - s.cancelStart) / CANCEL_MS);\n        const sy = 1 - 0.85 * easeOutCubic(k);\n        const sx = 1 + 0.3 * easeOutCubic(k);\n        const g = gooGroupRef.current;\n        if (g) {\n          // Slump anchored at the document line: widen while flattening,\n          // losing surface tension as it drains.\n          g.setAttribute(\"transform\", anchoredScale(CX, LINE_Y, sx, sy));\n          g.setAttribute(\"opacity\", (1 - easeInCubic(k)).toFixed(3));\n        }\n        const base = easeOutCubic(s.cancelP);\n        GOO_BLOBS.forEach((b, i) => {\n          const c = gooCircle(i);\n          if (c) c.setAttribute(\"r\", (b.r * base * (1 - 0.55 * k)).toFixed(2));\n        });\n        if (k >= 1) {\n          hideGoo();\n          setPhaseBoth(\"idle\");\n        }\n      } else if (s.phase === \"stamping\") {\n        const e = now - s.stampStart;\n        const stamp = stampOuterRef.current;\n        if (e < DROP_MS) {\n          // Fast ease-in drop; the still-molten blob keeps wobbling beneath.\n          const y = STAMP_RAISE * (1 - easeInCubic(e / DROP_MS));\n          stamp?.setAttribute(\"transform\", `translate(0 ${y.toFixed(2)})`);\n          writeGooFrame(1);\n        } else {\n          if (!s.impactDone) impact();\n          const u = Math.min(1, (e - DROP_MS) / SQUASH_MS);\n          const stampSy = squashScale(u, 0.76, 1.05);\n          const stampSx = 1 + (1 - stampSy) * 0.4;\n          stampSquashRef.current?.setAttribute(\n            \"transform\",\n            anchoredScale(CX, STAMP_ANCHOR_Y, stampSx, stampSy)\n          );\n          stamp?.setAttribute(\"transform\", \"translate(0 0)\");\n          const discSy = squashScale(u, 0.84, 1.02);\n          const discSx = 1 + (1 - discSy) * 0.5;\n          sealGroupRef.current?.setAttribute(\n            \"transform\",\n            anchoredScale(CX, CY + SEAL_R, discSx, discSy)\n          );\n          // Squish ring: displaced wax puffs outward from the rim and fades.\n          const pu = Math.min(1, (e - DROP_MS) / PUFF_MS);\n          PUFFS.forEach((p, i) => {\n            const c = puffRefs.current[i];\n            if (!c) return;\n            const d = SEAL_R + 17 * easeOutCubic(pu);\n            c.setAttribute(\"cx\", (CX + p.cos * d).toFixed(2));\n            c.setAttribute(\"cy\", (CY + p.sin * d * 0.8).toFixed(2));\n            c.setAttribute(\"r\", (3.4 * (1 - pu)).toFixed(2));\n            c.setAttribute(\"opacity\", (0.85 * (1 - pu)).toFixed(3));\n          });\n          if (e >= DROP_MS + SQUASH_MS) {\n            const ru = Math.min(1, (e - DROP_MS - SQUASH_MS) / RETRACT_MS);\n            stamp?.setAttribute(\n              \"transform\",\n              `translate(0 ${(STAMP_RAISE * easeOutCubic(ru)).toFixed(2)})`\n            );\n            stamp?.setAttribute(\"opacity\", (1 - ru).toFixed(3));\n            if (ru >= 1) {\n              stamp?.setAttribute(\"opacity\", \"0\");\n              stampSquashRef.current?.removeAttribute(\"transform\");\n              sealGroupRef.current?.removeAttribute(\"transform\");\n              hidePuffs();\n              setPhaseBoth(\"cooling\");\n              s.coolStart = now;\n            }\n          }\n        }\n      } else if (s.phase === \"cooling\") {\n        const e = now - s.coolStart;\n        const u = Math.min(1, e / COOL_MS);\n        discRef.current?.setAttribute(\"fill\", lerpWax(u));\n        sheenRef.current?.setAttribute(\"opacity\", (0.4 * (1 - u)).toFixed(3));\n        crackRefs.current.forEach((c, i) => {\n          if (!c) return;\n          const L = s.crackLens[i] ?? 0;\n          const lu = Math.min(\n            1,\n            Math.max(0, (e - i * CRACK_STAGGER_MS) / CRACK_MS)\n          );\n          c.setAttribute(\n            \"stroke-dashoffset\",\n            (L * (1 - easeOutCubic(lu))).toFixed(2)\n          );\n        });\n        if (u >= 1) {\n          finalizeSealed();\n        }\n      }\n\n      if (\n        s.phase === \"pouring\" ||\n        s.phase === \"cancelling\" ||\n        s.phase === \"stamping\" ||\n        s.phase === \"cooling\"\n      ) {\n        s.raf = requestAnimationFrame(tick);\n      } else {\n        s.raf = 0;\n      }\n    };\n\n    const wake = () => {\n      if (s.raf) return;\n      s.last = performance.now();\n      s.raf = requestAnimationFrame(tick);\n    };\n\n    const startHold = (source: HoldSource) => {\n      if (s.phase !== \"idle\" || s.holdSource !== null) return;\n      s.holdSource = source;\n      s.progress = 0;\n      s.t = 0;\n      setPhaseBoth(\"pouring\");\n      announce(\"Poured\");\n      if (!s.reduced) {\n        const g = gooGroupRef.current;\n        if (g) {\n          g.setAttribute(\"opacity\", \"1\");\n          g.removeAttribute(\"transform\");\n        }\n      }\n      wake();\n    };\n\n    const release = (source: HoldSource) => {\n      if (s.holdSource !== source) return;\n      s.holdSource = null;\n      if (s.phase !== \"pouring\") return;\n      announce(\"Cancelled\");\n      if (s.reduced) {\n        hideGoo();\n        setPhaseBoth(\"idle\");\n        return;\n      }\n      s.cancelStart = performance.now();\n      s.cancelP = s.progress;\n      setPhaseBoth(\"cancelling\");\n      wake();\n    };\n\n    // Measure crack lengths once, in RAW user units (getTotalLength on the\n    // authored geometry — no pathLength attribute anywhere), and park each\n    // crack fully hidden behind its own dash offset.\n    crackRefs.current.forEach((c, i) => {\n      if (!c) return;\n      const L = c.getTotalLength();\n      s.crackLens[i] = L;\n      c.setAttribute(\"stroke-dasharray\", L.toFixed(2));\n      c.setAttribute(\"stroke-dashoffset\", L.toFixed(2));\n    });\n\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onMq = () => {\n      s.reduced = mq.matches;\n    };\n    onMq();\n    mq.addEventListener(\"change\", onMq);\n\n    const onPointerDown = (e: PointerEvent) => {\n      if (s.phase === \"sealed\") return;\n      btn.setPointerCapture(e.pointerId);\n      startHold(\"real\");\n    };\n    const onPointerEnd = () => release(\"real\");\n    const onKeyDown = (e: KeyboardEvent) => {\n      if ((e.key === \" \" || e.key === \"Enter\") && !e.repeat) {\n        e.preventDefault();\n        if (s.phase === \"sealed\") return;\n        startHold(\"real\");\n      }\n    };\n    const onKeyUp = (e: KeyboardEvent) => {\n      if (e.key === \" \" || e.key === \"Enter\") release(\"real\");\n    };\n    const onBlur = () => release(\"real\");\n\n    btn.addEventListener(\"pointerdown\", onPointerDown);\n    btn.addEventListener(\"pointerup\", onPointerEnd);\n    btn.addEventListener(\"pointercancel\", onPointerEnd);\n    btn.addEventListener(\"pointerleave\", onPointerEnd);\n    btn.addEventListener(\"lostpointercapture\", onPointerEnd);\n    btn.addEventListener(\"keydown\", onKeyDown);\n    btn.addEventListener(\"keyup\", onKeyUp);\n    btn.addEventListener(\"blur\", onBlur);\n\n    // Scripted access for the self-driving demo — the exact same internal\n    // hold/release functions the real handlers use. Synthetic starts no-op\n    // while a real hold is active (startHold requires idle + no source) and\n    // a synthetic release can never cancel a real hold (source mismatch).\n    apiRef.current = {\n      start: () => startHold(\"synthetic\"),\n      release: () => release(\"synthetic\"),\n    };\n\n    return () => {\n      cancelAnimationFrame(s.raf);\n      s.raf = 0;\n      mq.removeEventListener(\"change\", onMq);\n      btn.removeEventListener(\"pointerdown\", onPointerDown);\n      btn.removeEventListener(\"pointerup\", onPointerEnd);\n      btn.removeEventListener(\"pointercancel\", onPointerEnd);\n      btn.removeEventListener(\"pointerleave\", onPointerEnd);\n      btn.removeEventListener(\"lostpointercapture\", onPointerEnd);\n      btn.removeEventListener(\"keydown\", onKeyDown);\n      btn.removeEventListener(\"keyup\", onKeyUp);\n      btn.removeEventListener(\"blur\", onBlur);\n      apiRef.current = null;\n    };\n  }, [announce]);\n\n  // Demo timeline: two short holds that release early (slump) while the\n  // automated gate screenshots a normal idle button, then one full hold\n  // (~5.7s in) that completes, stamps, cools and stays sealed forever.\n  useEffect(() => {\n    if (!demo) return;\n    const timers: number[] = [];\n    const at = (ms: number, fn: () => void) => {\n      timers.push(window.setTimeout(fn, ms));\n    };\n    at(900, () => apiRef.current?.start());\n    at(1520, () => apiRef.current?.release());\n    at(2800, () => apiRef.current?.start());\n    at(3360, () => apiRef.current?.release());\n    at(5700, () => apiRef.current?.start());\n    at(5700 + holdMs + 250, () => apiRef.current?.release());\n    return () => timers.forEach((t) => window.clearTimeout(t));\n  }, [demo, holdMs]);\n\n  const sealed = phase === \"sealed\";\n\n  return (\n    <div className={`flex flex-col items-center gap-4 ${className}`}>\n      {/* Dedicated announcer: only the throttle-free phase messages live\n          here, never the button label. */}\n      <span role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {announceText}\n      </span>\n\n      <svg\n        width={280}\n        height={170}\n        viewBox=\"0 0 280 170\"\n        aria-hidden=\"true\"\n        focusable=\"false\"\n        className=\"pointer-events-none select-none\"\n      >\n        <defs>\n          {/* Classic goo: heavy blur, then an alpha threshold that fuses\n              overlapping circles into one liquid silhouette. */}\n          <filter id={gooId} x=\"-40%\" y=\"-40%\" width=\"180%\" height=\"180%\">\n            <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"5\" result=\"blur\" />\n            <feColorMatrix\n              in=\"blur\"\n              type=\"matrix\"\n              values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7\"\n            />\n          </filter>\n        </defs>\n\n        {/* Document edge: the tail of a letter, wax lands on the last rule. */}\n        <line x1={18} y1={100} x2={96} y2={100} stroke=\"var(--border)\" strokeWidth={2} />\n        <line x1={18} y1={LINE_Y} x2={262} y2={LINE_Y} stroke=\"var(--border)\" strokeWidth={2} />\n        <line x1={18} y1={140} x2={110} y2={140} stroke=\"var(--border)\" strokeWidth={2} />\n\n        {/* Molten pour: gooey-fused circles, radii driven by hold progress,\n            positions jittered by the wobble clock. */}\n        <g ref={gooGroupRef} opacity={0} filter={`url(#${gooId})`}>\n          {GOO_BLOBS.map((b, i) => (\n            <circle\n              key={i}\n              ref={(el) => {\n                gooRefs.current[i] = el;\n              }}\n              cx={b.x}\n              cy={b.y}\n              r={0}\n              fill={WAX_MOLTEN}\n            />\n          ))}\n        </g>\n\n        {/* Squish ring: displaced wax at the moment of impact. */}\n        <g>\n          {PUFFS.map((_, i) => (\n            <circle\n              key={i}\n              ref={(el) => {\n                puffRefs.current[i] = el;\n              }}\n              cx={CX}\n              cy={CY}\n              r={0}\n              fill={WAX_MOLTEN}\n              opacity={0}\n            />\n          ))}\n        </g>\n\n        {/* The pressed seal: scalloped disc, embossed monogram, sheen,\n            micro-cracks. Hidden until the stamp lands. */}\n        <g ref={sealGroupRef} opacity={0}>\n          <path ref={discRef} d={SEAL_PATH} fill={WAX_HOT} />\n          <path\n            d={SEAL_RIM}\n            fill=\"none\"\n            stroke={WAX_SHADOW}\n            strokeOpacity={0.4}\n            strokeWidth={1}\n          />\n          <circle\n            cx={CX}\n            cy={CY}\n            r={20}\n            fill=\"none\"\n            stroke={WAX_HIGHLIGHT}\n            strokeOpacity={0.28}\n            strokeWidth={1}\n          />\n          {/* Emboss: two offset copies — dark shadow down-right, light\n              highlight up-left — faking a bevel-carved monogram. */}\n          <text\n            x={CX + 0.9}\n            y={CY + 0.9}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            className=\"font-mono\"\n            fontSize={24}\n            fontWeight={700}\n            fill={WAX_SHADOW}\n          >\n            S\n          </text>\n          <text\n            x={CX - 0.9}\n            y={CY - 0.9}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            className=\"font-mono\"\n            fontSize={24}\n            fontWeight={700}\n            fill={WAX_HIGHLIGHT}\n            fillOpacity={0.9}\n          >\n            S\n          </text>\n          {/* Hairline micro-cracks — raw-unit dash reveals, staggered. */}\n          {CRACK_PATHS.map((d, i) => (\n            <path\n              key={i}\n              ref={(el) => {\n                crackRefs.current[i] = el;\n              }}\n              d={d}\n              fill=\"none\"\n              stroke={WAX_SHADOW}\n              strokeOpacity={0.85}\n              strokeWidth={0.9}\n              strokeLinecap=\"round\"\n            />\n          ))}\n          {/* Specular sheen — dies as the wax cools. */}\n          <ellipse\n            ref={sheenRef}\n            cx={130}\n            cy={102}\n            rx={9.5}\n            ry={4.5}\n            transform=\"rotate(-28 130 102)\"\n            fill=\"#ffffff\"\n            opacity={0}\n          />\n        </g>\n\n        {/* The signet stamp: parked above the stage, drops on completion. */}\n        <g ref={stampOuterRef} opacity={0} transform={`translate(0 ${STAMP_RAISE})`}>\n          <g ref={stampSquashRef}>\n            <rect\n              x={133}\n              y={40}\n              width={14}\n              height={38}\n              rx={6}\n              fill=\"var(--surface)\"\n              stroke=\"var(--ns-muted)\"\n              strokeWidth={1.3}\n            />\n            <rect\n              x={116}\n              y={74}\n              width={48}\n              height={12}\n              rx={6}\n              fill=\"var(--surface)\"\n              stroke=\"var(--ns-muted)\"\n              strokeWidth={1.3}\n            />\n            <line x1={124} y1={80} x2={156} y2={80} stroke=\"var(--border)\" strokeWidth={1} />\n          </g>\n        </g>\n      </svg>\n\n      <div className=\"flex h-9 items-center gap-3\">\n        <button\n          ref={btnRef}\n          type=\"button\"\n          aria-disabled={sealed ? true : undefined}\n          className=\"select-none touch-none rounded-[6px] border border-border bg-surface px-5 py-2 text-sm font-medium text-foreground transition-[background-color,border-color,transform] duration-150 hover:border-ns-muted hover:bg-border/60 active:scale-[0.98] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent aria-disabled:cursor-default aria-disabled:opacity-45 aria-disabled:hover:border-border aria-disabled:hover:bg-surface\"\n        >\n          Seal\n        </button>\n        {sealed && (\n          <span className=\"font-mono text-[11px] uppercase tracking-[0.2em] text-ns-muted\">\n            Sealed\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/confirm-hold-wax.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "confirm",
      "hold",
      "press-and-hold",
      "wax-seal",
      "svg",
      "gooey",
      "destructive",
      "aria-live",
      "accessibility"
    ],
    "instruction": "Build a press-and-hold confirmation as a molten wax seal on a document. The stage is a fixed 280x170 SVG: a --border document rule runs across at y=120 (plus two shorter rules above and below suggesting the tail of a letter), with the empty seal spot centered on it and a 'Seal' button below (standard chrome: --surface fill, --border border, --foreground text, an always-present plain CSS :hover border/background change independent of the wax visuals, and a visible focus-visible outline in --ns-accent). HOLD GRAMMAR (confirm-hold-ink's exact grammar, molten visuals): pointerdown on the button (with setPointerCapture) or non-repeat keydown Space/Enter while focused starts a tracked hold; a rAF loop advances progress 0-1 over ~1100ms of unclamped wall time written to a ref, never React state; pointerup/pointercancel/pointerleave/lostpointercapture/keyup/blur before 1.0 is the cancel path; the instant progress reaches 1.0 the completion sequence fires regardless of any subsequent release. POURING: an SVG gooey filter (feGaussianBlur stdDeviation 5 piped into an feColorMatrix alpha threshold '... 18 -7') fuses three overlapping circles sitting on the rule into one liquid silhouette in molten crimson (component-local constants around #b31b30 — the repo has no crimson token and the loud collection is color-exempt; everything that is not wax stays on the theme tokens). Circle radii grow from ~0 to target (22/14/13u) on an ease-out of hold progress, and while liquid their cx/cy jitter a few px on sin/cos of an internal ms clock (per-circle phase offsets, amplitude scaled by progress) — all direct setAttribute writes on circle refs each frame. STAMPING (~690ms total): a signet stamp (handle + base rects in --surface/--ns-muted, parked 92u above, opacity 0) drops with a fast ease-in translateY over 190ms onto the blob; at impact the goo hides and a STATIC scalloped seal disc appears — a precomputed path whose perimeter is a circle radius-modulated by a low-frequency cosine (10 scallops, amp 2.6 on r=29) — while both stamp and disc play a squash-and-rebound (stamp scaleY 0.76 -> 1.05 -> 1, disc 0.84 -> 1.02 -> 1, each anchored at its base via translate-scale-translate transform attributes, x widening inversely) and a squish ring of 8 small wax circles puffs outward from the rim (distance +17u ease-out, radius and opacity to 0 over 380ms). The disc carries an embossed monogram faked with exactly two overlapping copies of the glyph offset ~0.9u in opposite directions — a lighter highlight up-left, a darker shadow down-right — plus a scalloped inner rim stroke and a faint highlight ring for bevel depth. The stamp then retracts upward and fades over 240ms. COOLING (exactly 2000ms, JS-interpolated in the same rAF loop): a white specular sheen ellipse fades from 0.4 to 0, the disc fill lerps per-channel from hot crimson (#9c1526) to deep cooled crimson (#570d18), and three hairline micro-crack paths etch in via stroke-dasharray/stroke-dashoffset animating from fully hidden to revealed, staggered 420ms apart over 700ms each. CRITICAL: the crack lengths come from getTotalLength() on the authored geometry and the dash values are written in RAW SVG user units — never combine an SVG pathLength attribute with vector-effect non-scaling-stroke, and use neither here. EARLY RELEASE: the blob slumps — the goo group scales anchored at the document line (scaleY toward 0.15, scaleX widening ~1.3, radii shrinking, opacity easing to 0 over ~520ms, like losing surface tension) — then the stage returns fully to idle and the button re-arms. TERMINAL STATE: sealed is permanent — no reset, no replay. The disc stays exactly as cooled, the button keeps its visible 'Seal' text (accessible name intact throughout the lifecycle) but becomes aria-disabled='true', visually dimmed, with all handlers guarded, and a visible mono 'Sealed' caption appears beside it. ACCESSIBILITY: a dedicated sr-only span (role=status, aria-live=polite, aria-atomic=true), separate from the button, announces 'Poured' when a hold starts, 'Cancelled' on early release and 'Sealed' at the terminal state, appending a zero-width-space parity toggle so a repeated identical message (two cancels in a row) still forces a text-node change and re-announces. The SVG stage is aria-hidden and pointer-events-none; the button is the only interactive element. REDUCED MOTION (matchMedia prefers-reduced-motion): the hold requirement is preserved — intent still takes the full hold — but every intermediate visual is skipped: no goo growth or wobble, no stamp drop, no cooling transition; the moment progress reaches 1.0 the fully cooled, cracked seal appears in one discrete step, and a cancel snaps straight back to idle. DEMO MODE: a demo prop runs a scripted setTimeout timeline through the exact same internal hold/release functions the real handlers use — two short early-release holds (~620ms and ~560ms of a 1100ms threshold) inside the first ~3.5s so automated hover/focus screenshots see a normal idle button while still showing the molten wobble and slump, then one full hold at ~5.7s that completes, stamps, cools and stays sealed permanently; synthetic starts no-op unless the component is idle with no active hold, and a synthetic release can never cancel a real hold (hold-source tagging), so scripted playback never fights real input. Zero dependencies, no canvas."
  },
  "type": "registry:ui"
}