{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "seam-gild",
  "title": "Seam Gild",
  "description": "A kintsugi payoff for consequential actions: confirming cracks the panel with a biased-random-walk SVG path in ~400ms, then immediately re-fills it from both ends as a bright raised seam. The scar stays for the rest of the browser session, so repeated confirmations accumulate as ornament instead of fading. Before any confirmation, a faint stress hairline draws in, holds, and dissolves on its own every few seconds, so the panel never sits as a dead frame.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/seam-gild/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// SeamGild — a kintsugi payoff for consequential actions. Confirming a\n// weighty action (payment cleared, migration finished, plan upgraded) fires\n// a hairline crack that races across the panel in ~400ms as a biased random\n// walk with one branch, then is immediately re-filled from both ends toward\n// its middle as a bright raised seam. The seam stays for the rest of the\n// browser session — repeated confirmations accumulate distinct scars, so the\n// panel's history becomes visible ornament instead of a moment that fades.\n//\n// Distinct on purpose from two siblings that share the \"weighty confirm\"\n// slot: success-nucleation grows an ordered crystal out of a supercooled\n// field (order emerging from disorder, and nothing persists between\n// successes — the field just resets). confirm-slide-shatter destroys the\n// pane on commit (glass explodes, the surface is gone). SeamGild repairs:\n// the panel is damaged and then visibly mended, and the mend is the only\n// trace left once the motion stops — successive confirmations accumulate as\n// ornament instead of evaporating or resetting.\n//\n// MECHANISM: on confirm, one seed point is picked on the panel's border\n// (retried against a measured exclusion list — the content block, the\n// button, the status line — so the crack never starts inside one of them),\n// then a short biased random walk grows inward: each step nudges the\n// previous heading by a small jitter, and any step that would land inside\n// an excluded rect resamples its angle a few times before the walk just\n// stops early — a shorter, valid crack rather than an invalid one. Partway\n// along, one branch forks off at roughly a 60deg turn and walks a shorter\n// distance under the same avoidance.\n//\n// Both polylines render as SVG <path>s with pathLength=1 (so the real pixel\n// length never has to be measured): dasharray \"1\", dashoffset animated\n// 1 -> 0 over 400ms in --border is the crack drawing itself outward from\n// the origin — the branch runs on the same 400ms clock but starts partway\n// through, so it visibly forks off the trunk mid-propagation rather than\n// growing from t=0 alongside it.\n//\n// The instant the crack finishes, the gild pass starts: the same polyline,\n// once forward and once with its points reversed, both pathLength=1\n// dashoffset 1 -> 0.5 over ~450ms in --foreground at 1.5px — forward\n// reveals the first half from the origin, reversed reveals the first half\n// of the flipped path (= the original's second half) from the far end, so\n// the bright seam visibly closes toward the middle from both ends at once.\n// A `drop-shadow(0 -1px 0 …)` filter on that same stroke is the raised\n// highlight — a 1px offset duplicate of exactly the currently-revealed\n// geometry, not a separate animated path. The original hairline crack\n// stays underneath as the seam's groove.\n//\n// PERSISTENCE: each finished seam's points (normalized to fractions of the\n// panel's box, so a later resize rescales instead of replaying stale pixel\n// coordinates) are appended to a capped list kept in sessionStorage under\n// `ns-seam-gild:<storageKey>`, alongside a running confirmation count under\n// the same key + `:count` — kept separately so the visible tally stays\n// accurate even on the rare panel too crowded to fit a new seam (the\n// confirmation still counts; it just had nowhere safe left to draw). Seams\n// loaded from storage render at their finished, gilded state directly, no\n// crack-then-gild replay — only the seam added in the current mount plays\n// the intro.\n//\n// A11Y: the crack/seam SVG layer is aria-hidden and pointer-events:none —\n// decoration only, absolutely positioned so it never participates in\n// layout or hit-testing. The real confirmation is a role=status\n// aria-live=assertive region that receives confirmedMessage the instant\n// the action resolves, entirely independent of whether the crack animation\n// plays, finishes, or even finds room to draw. A second, visible\n// role=status aria-live=polite line tracks the running seam count for\n// sighted users. prefers-reduced-motion skips the crack/gild propagation\n// entirely and renders every new seam at its finished, gilded state\n// immediately — the same code path used for seams loaded from storage.\n// ---------------------------------------------------------------------------\n\ntype Pt = { x: number; y: number };\ntype Stage = \"preCrack\" | \"crack\" | \"preGild\" | \"gild\" | \"settled\";\n\ntype SeamRecord = {\n  id: string;\n  /** normalized [0..1] points, fractions of the panel box at creation time */\n  trunk: [number, number][];\n  branch: [number, number][] | null;\n};\n\nconst CRACK_MS = 400;\nconst GILD_MS = 450;\n// Idle stress hairline — the panel's \"alive at rest\" tell before any\n// confirmation exists. Same geometry engine as a real crack (buildSeam), a\n// fraction of the opacity, --border only (no --foreground, no gild\n// highlight), so it never risks reading as a finished seam or a rendering\n// glitch: draws in, holds faintly, dissolves, and a new one starts\n// elsewhere after a pause. Only shown while the panel has no real seams yet\n// — once one exists, the panel already has its own trace and doesn't also\n// need an ambient hint.\nconst IDLE_START_MS = 900;\nconst IDLE_APPEAR_MS = 1300;\nconst IDLE_HOLD_MS = 2600;\nconst IDLE_FADE_MS = 1200;\nconst IDLE_GAP_MS = 1400;\nconst IDLE_OPACITY = 0.42;\nconst BRANCH_START_FRAC = 0.42; // branch begins this far into the trunk's crack timeline\nconst STEP = 8; // px per walk step\nconst EXCLUDE_PAD = 6; // px padding around an excluded rect\nconst MAX_SEAMS = 5;\nconst MAX_STEP_ATTEMPTS = 6; // per-step retries before a walk just stops early\nconst MAX_START_ATTEMPTS = 20;\nconst EASE = \"cubic-bezier(0.4, 0, 0.2, 1)\";\nconst HIGHLIGHT_FILTER =\n  \"drop-shadow(0 -1px 0 color-mix(in srgb, var(--foreground) 55%, transparent))\";\n\nfunction clamp(v: number, lo: number, hi: number): number {\n  return Math.max(lo, Math.min(hi, v));\n}\n\nfunction pointInRects(x: number, y: number, rects: DOMRect[], pad: number): boolean {\n  return rects.some(\n    (r) => x > r.left - pad && x < r.right + pad && y > r.top - pad && y < r.bottom + pad\n  );\n}\n\n/** biased random walk: each step nudges the previous heading, resamples the\n * angle a few times if it would land inside an excluded rect, and simply\n * stops early once it runs out of retries. */\nfunction walk(\n  start: Pt,\n  startAngle: number,\n  steps: number,\n  w: number,\n  h: number,\n  avoid: DOMRect[]\n): Pt[] {\n  const pts: Pt[] = [start];\n  let { x, y } = start;\n  let angle = startAngle;\n  for (let i = 0; i < steps; i++) {\n    let placed = false;\n    for (let attempt = 0; attempt < MAX_STEP_ATTEMPTS && !placed; attempt++) {\n      const a = angle + (Math.random() - 0.5) * (0.45 + attempt * 0.5);\n      const nx = x + Math.cos(a) * STEP;\n      const ny = y + Math.sin(a) * STEP;\n      if (nx < 3 || nx > w - 3 || ny < 3 || ny > h - 3) {\n        angle += (Math.random() > 0.5 ? 1 : -1) * (Math.PI / 3);\n        continue;\n      }\n      if (pointInRects(nx, ny, avoid, EXCLUDE_PAD)) {\n        angle += (Math.random() - 0.5) * 1.8;\n        continue;\n      }\n      x = nx;\n      y = ny;\n      angle = a;\n      pts.push({ x, y });\n      placed = true;\n    }\n    if (!placed) break;\n  }\n  return pts;\n}\n\nfunction pickStart(w: number, h: number, avoid: DOMRect[]): { pt: Pt; angle: number } | null {\n  const cx = w / 2;\n  const cy = h / 2;\n  for (let i = 0; i < MAX_START_ATTEMPTS; i++) {\n    const edge = Math.floor(Math.random() * 4);\n    let pt: Pt;\n    if (edge === 0) pt = { x: Math.random() * w, y: 4 };\n    else if (edge === 1) pt = { x: w - 4, y: Math.random() * h };\n    else if (edge === 2) pt = { x: Math.random() * w, y: h - 4 };\n    else pt = { x: 4, y: Math.random() * h };\n    if (pointInRects(pt.x, pt.y, avoid, EXCLUDE_PAD)) continue;\n    const angle = Math.atan2(cy - pt.y, cx - pt.x) + (Math.random() - 0.5) * 0.8;\n    return { pt, angle };\n  }\n  return null;\n}\n\n/** builds a trunk + one branch, both avoidance-aware, in panel-pixel space */\nfunction buildSeam(\n  w: number,\n  h: number,\n  avoid: DOMRect[]\n): { trunk: Pt[]; branch: Pt[] | null } | null {\n  if (w < 40 || h < 40) return null;\n  const start = pickStart(w, h, avoid);\n  if (!start) return null;\n  const diag = Math.hypot(w, h);\n  const steps = clamp(Math.round((diag / STEP) * 0.55), 6, 40);\n  const trunk = walk(start.pt, start.angle, steps, w, h, avoid);\n  if (trunk.length < 3) return null;\n\n  const bi = clamp(Math.floor(trunk.length * (0.35 + Math.random() * 0.3)), 1, trunk.length - 2);\n  const before = trunk[bi - 1]!;\n  const at = trunk[bi]!;\n  const trunkAngle = Math.atan2(at.y - before.y, at.x - before.x);\n  const side = Math.random() < 0.5 ? 1 : -1;\n  const branchAngle = trunkAngle + side * (0.9 + Math.random() * 0.35);\n  const branchSteps = clamp(Math.round((trunk.length - bi) * 0.9), 3, 20);\n  const branchRaw = walk(at, branchAngle, branchSteps, w, h, avoid);\n  const branch = branchRaw.length >= 3 ? branchRaw : null;\n\n  return { trunk, branch };\n}\n\nfunction toPath(pts: Pt[]): string {\n  return pts.map((p, i) => `${i === 0 ? \"M\" : \"L\"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n}\n\nfunction normalize(pts: Pt[], w: number, h: number): [number, number][] {\n  return pts.map((p) => [p.x / w, p.y / h]);\n}\n\nfunction denormalize(pts: [number, number][], w: number, h: number): Pt[] {\n  return pts.map(([nx, ny]) => ({ x: nx * w, y: ny * h }));\n}\n\nfunction useReducedMotion(): boolean {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nfunction storageKeyFor(storageKey: string) {\n  return `ns-seam-gild:${storageKey}`;\n}\n\nfunction readSeams(storageKey: string): SeamRecord[] {\n  try {\n    const raw = window.sessionStorage.getItem(storageKeyFor(storageKey));\n    if (!raw) return [];\n    const parsed = JSON.parse(raw);\n    return Array.isArray(parsed) ? parsed : [];\n  } catch {\n    return [];\n  }\n}\n\nfunction writeSeams(storageKey: string, seams: SeamRecord[]) {\n  try {\n    window.sessionStorage.setItem(storageKeyFor(storageKey), JSON.stringify(seams));\n  } catch {\n    // storage unavailable (private mode, quota) — the seam still renders for\n    // this mount, it just won't survive a reload. Not fatal.\n  }\n}\n\nfunction readCount(storageKey: string): number {\n  try {\n    const raw = window.sessionStorage.getItem(`${storageKeyFor(storageKey)}:count`);\n    return raw ? parseInt(raw, 10) || 0 : 0;\n  } catch {\n    return 0;\n  }\n}\n\nfunction writeCount(storageKey: string, n: number) {\n  try {\n    window.sessionStorage.setItem(`${storageKeyFor(storageKey)}:count`, String(n));\n  } catch {\n    // see writeSeams\n  }\n}\n\n// ---------------------------------------------------------------------------\n\n/** one gild stroke pair (forward + reversed) for a single polyline segment */\nfunction GildSegment({ pts, offset }: { pts: Pt[]; offset: number }) {\n  const forwardD = toPath(pts);\n  const reversedD = toPath([...pts].reverse());\n  const style = {\n    strokeDashoffset: offset,\n    transition: `stroke-dashoffset ${GILD_MS}ms ${EASE}`,\n    filter: HIGHLIGHT_FILTER,\n  } as const;\n  return (\n    <>\n      <path\n        d={forwardD}\n        pathLength={1}\n        fill=\"none\"\n        stroke=\"var(--foreground)\"\n        strokeWidth={1.5}\n        strokeLinecap=\"round\"\n        strokeDasharray={1}\n        style={style}\n      />\n      <path\n        d={reversedD}\n        pathLength={1}\n        fill=\"none\"\n        stroke=\"var(--foreground)\"\n        strokeWidth={1.5}\n        strokeLinecap=\"round\"\n        strokeDasharray={1}\n        style={style}\n      />\n    </>\n  );\n}\n\nfunction SeamMark({\n  trunk,\n  branch,\n  playIntro,\n}: {\n  trunk: Pt[];\n  branch: Pt[] | null;\n  playIntro: boolean;\n}) {\n  const [stage, setStage] = useState<Stage>(playIntro ? \"preCrack\" : \"settled\");\n\n  useEffect(() => {\n    if (stage === \"preCrack\") {\n      const raf = requestAnimationFrame(() => setStage(\"crack\"));\n      return () => cancelAnimationFrame(raf);\n    }\n    if (stage === \"crack\") {\n      const t = window.setTimeout(() => setStage(\"preGild\"), CRACK_MS + 20);\n      return () => window.clearTimeout(t);\n    }\n    if (stage === \"preGild\") {\n      const raf = requestAnimationFrame(() => setStage(\"gild\"));\n      return () => cancelAnimationFrame(raf);\n    }\n    if (stage === \"gild\") {\n      const t = window.setTimeout(() => setStage(\"settled\"), GILD_MS + 20);\n      return () => window.clearTimeout(t);\n    }\n  }, [stage]);\n\n  const trunkOffset = stage === \"preCrack\" ? 1 : 0;\n  const branchOffset = stage === \"preCrack\" ? 1 : 0;\n  const showGild = stage === \"preGild\" || stage === \"gild\" || stage === \"settled\";\n  const gildOffset = stage === \"preGild\" ? 1 : 0.5;\n\n  return (\n    <g>\n      {/* crack: the groove, stays visible under the gild */}\n      <path\n        d={toPath(trunk)}\n        pathLength={1}\n        fill=\"none\"\n        stroke=\"var(--border)\"\n        strokeWidth={1}\n        strokeLinecap=\"round\"\n        strokeDasharray={1}\n        style={{\n          strokeDashoffset: trunkOffset,\n          transition: `stroke-dashoffset ${CRACK_MS}ms ${EASE}`,\n        }}\n      />\n      {branch && (\n        <path\n          d={toPath(branch)}\n          pathLength={1}\n          fill=\"none\"\n          stroke=\"var(--border)\"\n          strokeWidth={1}\n          strokeLinecap=\"round\"\n          strokeDasharray={1}\n          style={{\n            strokeDashoffset: branchOffset,\n            transition: `stroke-dashoffset ${CRACK_MS * (1 - BRANCH_START_FRAC)}ms ${EASE}`,\n            transitionDelay: `${CRACK_MS * BRANCH_START_FRAC}ms`,\n          }}\n        />\n      )}\n\n      {/* gild: bright raised seam, re-filling inward from both ends */}\n      {showGild && (\n        <>\n          <GildSegment pts={trunk} offset={gildOffset} />\n          {branch && <GildSegment pts={branch} offset={gildOffset} />}\n        </>\n      )}\n    </g>\n  );\n}\n\n/** one hairline draw-in, mirroring SeamMark's own preCrack -> crack tick so\n * the transition actually animates instead of snapping on mount. */\nfunction IdleStressStroke({ trunk }: { trunk: Pt[] }) {\n  const [drawn, setDrawn] = useState(false);\n  useEffect(() => {\n    const raf = requestAnimationFrame(() => setDrawn(true));\n    return () => cancelAnimationFrame(raf);\n  }, []);\n  return (\n    <path\n      d={toPath(trunk)}\n      pathLength={1}\n      fill=\"none\"\n      stroke=\"var(--border)\"\n      strokeWidth={1}\n      strokeLinecap=\"round\"\n      strokeDasharray={1}\n      style={{\n        strokeDashoffset: drawn ? 0 : 1,\n        transition: `stroke-dashoffset ${IDLE_APPEAR_MS}ms ${EASE}`,\n      }}\n    />\n  );\n}\n\ntype IdleStage = \"hidden\" | \"appear\" | \"hold\" | \"fade\";\n\n/** the pristine panel's idle tell — see the constants above for why this\n * exists and stays this restrained. */\nfunction IdleStress({\n  panelRef,\n  box,\n}: {\n  panelRef: { current: HTMLDivElement | null };\n  box: { w: number; h: number };\n}) {\n  const [trunk, setTrunk] = useState<Pt[] | null>(null);\n  const [cycleKey, setCycleKey] = useState(0);\n  const [stage, setStage] = useState<IdleStage>(\"hidden\");\n\n  useEffect(() => {\n    if (box.w <= 0 || box.h <= 0) return;\n    let cancelled = false;\n    let timer: number | undefined;\n\n    const schedule = (fn: () => void, ms: number) => {\n      timer = window.setTimeout(() => {\n        if (!cancelled) fn();\n      }, ms);\n    };\n\n    const tryBuild = () => {\n      const panel = panelRef.current;\n      if (!panel) {\n        schedule(tryBuild, IDLE_GAP_MS);\n        return;\n      }\n      const panelRect = panel.getBoundingClientRect();\n      const avoid = Array.from(panel.querySelectorAll<HTMLElement>(\"[data-seam-avoid]\")).map(\n        (el) => {\n          const r = el.getBoundingClientRect();\n          return new DOMRect(r.left - panelRect.left, r.top - panelRect.top, r.width, r.height);\n        }\n      );\n      const built = buildSeam(box.w, box.h, avoid);\n      if (!built) {\n        schedule(tryBuild, IDLE_GAP_MS); // too crowded this cycle — retry after the usual gap\n        return;\n      }\n      setTrunk(built.trunk);\n      setCycleKey((k) => k + 1);\n      setStage(\"appear\");\n      schedule(() => {\n        setStage(\"hold\");\n        schedule(fadeOut, IDLE_HOLD_MS);\n      }, IDLE_APPEAR_MS);\n    };\n    const fadeOut = () => {\n      setStage(\"fade\");\n      schedule(() => {\n        setStage(\"hidden\");\n        schedule(tryBuild, IDLE_GAP_MS);\n      }, IDLE_FADE_MS);\n    };\n\n    schedule(tryBuild, IDLE_START_MS);\n    return () => {\n      cancelled = true;\n      window.clearTimeout(timer);\n    };\n  }, [box.w, box.h, panelRef]);\n\n  if (!trunk || stage === \"hidden\") return null;\n  return (\n    <g\n      style={{\n        opacity: stage === \"fade\" ? 0 : IDLE_OPACITY,\n        transition: `opacity ${stage === \"fade\" ? IDLE_FADE_MS : IDLE_APPEAR_MS}ms ${EASE}`,\n      }}\n    >\n      <IdleStressStroke key={cycleKey} trunk={trunk} />\n    </g>\n  );\n}\n\n// ---------------------------------------------------------------------------\n\nexport interface SeamGildProps {\n  /** idle button label — the weighty action being confirmed. default \"Confirm payment\" */\n  actionLabel?: string;\n  /** button label while awaiting an async onConfirm. default \"Processing…\" */\n  pendingLabel?: string;\n  /** the real confirmation: fired into an aria-live=assertive region the instant the action resolves, independent of the visual. default \"Payment cleared.\" */\n  confirmedMessage?: string;\n  /** short description shown in the panel body */\n  description?: string;\n  /** called on trigger; return a Promise and the seam fires on resolve (reverts to idle on reject) */\n  onConfirm?: () => void | Promise<void>;\n  /** sessionStorage namespace — give concurrent panels on one page distinct keys. default \"panel\" */\n  storageKey?: string;\n  /** how many seams the panel keeps before the oldest is retired. default 5 */\n  maxSeams?: number;\n  /** bump to wipe this panel's stored seams and start fresh */\n  resetKey?: number;\n  className?: string;\n}\n\nexport function SeamGild({\n  actionLabel = \"Confirm payment\",\n  pendingLabel = \"Processing…\",\n  confirmedMessage = \"Payment cleared.\",\n  description = \"One tap commits the charge. Every confirmation leaves its own mark on this panel for the rest of the session.\",\n  onConfirm,\n  storageKey = \"panel\",\n  maxSeams = MAX_SEAMS,\n  resetKey = 0,\n  className = \"\",\n}: SeamGildProps) {\n  const reduced = useReducedMotion();\n  const panelRef = useRef<HTMLDivElement>(null);\n  const [box, setBox] = useState({ w: 0, h: 0 });\n  const [seams, setSeams] = useState<SeamRecord[]>([]);\n  const [newestId, setNewestId] = useState<string | null>(null);\n  const [pending, setPending] = useState(false);\n  const [announcement, setAnnouncement] = useState(\"\");\n  const [count, setCount] = useState(0);\n  const [pendingBuild, setPendingBuild] = useState(0);\n  const idRef = useRef(0);\n  const onConfirmRef = useRef(onConfirm);\n  onConfirmRef.current = onConfirm;\n\n  // load persisted seams for this key on mount / when storageKey or resetKey changes\n  useEffect(() => {\n    if (resetKey > 0) {\n      writeSeams(storageKey, []);\n      writeCount(storageKey, 0);\n      setSeams([]);\n      setCount(0);\n    } else {\n      setSeams(readSeams(storageKey));\n      setCount(readCount(storageKey));\n    }\n    setNewestId(null);\n  }, [storageKey, resetKey]);\n\n  useEffect(() => {\n    const panel = panelRef.current;\n    if (!panel) return;\n    const measure = () => {\n      const r = panel.getBoundingClientRect();\n      setBox({ w: r.width, h: r.height });\n    };\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(panel);\n    return () => ro.disconnect();\n  }, []);\n\n  async function handleConfirm() {\n    if (pending) return;\n    const result = onConfirmRef.current?.();\n    if (result && typeof (result as Promise<void>).then === \"function\") {\n      setPending(true);\n      try {\n        await result;\n      } catch {\n        setPending(false);\n        return;\n      }\n      setPending(false);\n    }\n\n    // the real confirmation: fires no matter what happens to the visual below\n    const nextCount = readCount(storageKey) + 1;\n    writeCount(storageKey, nextCount);\n    setCount(nextCount);\n    setAnnouncement(confirmedMessage);\n\n    // defer the crack geometry to the layout effect below, which runs after\n    // this render (the count above included) has actually committed to the\n    // DOM — see its comment for why that ordering matters.\n    setPendingBuild((n) => n + 1);\n  }\n\n  // Builds a seam against exclusion rects measured from the DOM *after* the\n  // triggering render committed. Measuring inside handleConfirm instead\n  // (synchronously, before React flushes the state updates above) missed\n  // the counter on exactly the confirmation that makes it first appear —\n  // count goes 0 -> 1 in the same click that starts the crack, so\n  // `querySelectorAll(\"[data-seam-avoid]\")` ran against DOM that hadn't\n  // grown the counter span yet, and the crack could route straight through\n  // where the number was about to land.\n  useLayoutEffect(() => {\n    if (pendingBuild === 0) return; // nothing pending on initial mount\n    const panel = panelRef.current;\n    if (!panel || box.w <= 0 || box.h <= 0) return;\n    const panelRect = panel.getBoundingClientRect();\n    const localAvoid = Array.from(panel.querySelectorAll<HTMLElement>(\"[data-seam-avoid]\")).map(\n      (el) => {\n        const r = el.getBoundingClientRect();\n        return new DOMRect(r.left - panelRect.left, r.top - panelRect.top, r.width, r.height);\n      }\n    );\n    const built = buildSeam(box.w, box.h, localAvoid);\n    if (!built) return; // too crowded to fit a new seam this time — confirmation still counted above\n\n    idRef.current += 1;\n    const record: SeamRecord = {\n      id: `${Date.now()}-${idRef.current}`,\n      trunk: normalize(built.trunk, box.w, box.h),\n      branch: built.branch ? normalize(built.branch, box.w, box.h) : null,\n    };\n\n    setSeams((prev) => {\n      const next = [...prev, record].slice(-maxSeams);\n      writeSeams(storageKey, next);\n      return next;\n    });\n    setNewestId(record.id);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [pendingBuild]);\n\n  return (\n    <div\n      ref={panelRef}\n      className={`relative overflow-hidden rounded-[12px] border border-border bg-background p-6 ${className}`}\n    >\n      <svg\n        aria-hidden=\"true\"\n        role=\"presentation\"\n        className=\"pointer-events-none absolute inset-0 h-full w-full\"\n        viewBox={`0 0 ${Math.max(1, box.w)} ${Math.max(1, box.h)}`}\n        preserveAspectRatio=\"none\"\n      >\n        {box.w > 0 && box.h > 0 && seams.length === 0 && !reduced && (\n          <IdleStress panelRef={panelRef} box={box} />\n        )}\n        {box.w > 0 &&\n          box.h > 0 &&\n          seams.map((s) => (\n            <SeamMark\n              key={s.id}\n              trunk={denormalize(s.trunk, box.w, box.h)}\n              branch={s.branch ? denormalize(s.branch, box.w, box.h) : null}\n              playIntro={!reduced && s.id === newestId}\n            />\n          ))}\n      </svg>\n\n      <div data-seam-avoid className=\"relative flex flex-col gap-2\">\n        <p className=\"font-mono text-xs uppercase tracking-[0.2em] text-ns-muted\">\n          ns-ui / seam-gild\n        </p>\n        <p className=\"max-w-sm text-sm text-foreground\">{description}</p>\n      </div>\n\n      <div className=\"relative mt-6 flex flex-wrap items-center gap-4\">\n        <button\n          type=\"button\"\n          data-seam-gild-trigger\n          data-seam-avoid\n          onClick={handleConfirm}\n          disabled={pending}\n          className=\"rounded-[6px] border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors duration-150 hover:border-ns-accent/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent disabled:pointer-events-none disabled:opacity-60\"\n        >\n          {pending ? pendingLabel : actionLabel}\n        </button>\n\n        {count > 0 && (\n          <span\n            data-seam-gild-status\n            data-seam-avoid\n            role=\"status\"\n            aria-live=\"polite\"\n            className=\"font-mono text-xs text-ns-muted\"\n          >\n            {count} seam{count === 1 ? \"\" : \"s\"} this session\n          </span>\n        )}\n      </div>\n\n      {/* the real confirmation — independent of whether the visual seam drew */}\n      <span role=\"status\" aria-live=\"assertive\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/seam-gild.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": [
      "confirm",
      "success",
      "payment",
      "svg",
      "persistence",
      "session",
      "repair",
      "aria-live"
    ],
    "instruction": "A self-contained confirm panel: a bordered card with a description block, a real <button> (default label 'Confirm payment') and an aria-hidden, pointer-events:none SVG overlay absolutely positioned over the whole panel. Clicking the button calls the optional onConfirm callback — awaited if it returns a Promise, pendingLabel shown meanwhile — and the instant it settles, two things fire independently of each other: a role=status aria-live=assertive region receives confirmedMessage (this is the real confirmation, and it fires even if the visual below has nowhere to draw), and a visible role=status aria-live=polite counter ('N seams this session') updates from a running total kept in sessionStorage. Geometry: one seed point is picked on the panel's edge, retried up to 20 times against a measured exclusion list — every element carrying data-seam-avoid (the content block, the button, the counter) — read via getBoundingClientRect so the crack never starts inside one of them. A biased random walk grows inward from there: each step nudges the previous heading by a small jitter, and a step that would land inside an excluded rect (padded 6px) resamples its angle up to 6 times before the walk simply stops early — a shorter valid crack rather than an invalid one. Partway along (35-65% of the walk), one branch forks off at roughly a 60deg turn and walks a shorter distance under the same avoidance. Both polylines render as SVG <path>s with pathLength=1, so real pixel length is never measured: dasharray '1', dashoffset animated 1->0 over 400ms in --border draws the crack outward from the origin, with the branch on the same 400ms clock but starting 42% of the way through so it visibly forks mid-propagation rather than growing from t=0. The instant the crack's 400ms finishes, the gild pass starts: the same polyline once forward and once with its points reversed, both pathLength=1, dashoffset animated 1->0.5 over ~450ms in --foreground at 1.5px — forward reveals the first half from the origin, reversed reveals the first half of the flipped path (the original's second half) from the far end, so the bright seam visibly closes toward the middle from both directions at once. A drop-shadow(0 -1px 0 color-mix(in srgb, var(--foreground) 55%, transparent)) filter on that same stroke is the raised highlight, an offset duplicate of exactly the currently-revealed geometry rather than a separately animated path; the original hairline crack stays underneath as the seam's groove. Finished seams are normalized to fractions of the panel box and appended (capped at maxSeams, default 5, oldest dropped) to sessionStorage under ns-seam-gild:<storageKey>, alongside the running count under the same key plus ':count' kept separately so the tally stays accurate even on the rare panel too crowded to fit a new seam. Seams loaded from storage, or any seam past its own mount, render at their finished gilded state directly with no crack-then-gild replay. prefers-reduced-motion skips the propagation entirely for every new seam and renders it settled immediately, the same code path storage-loaded seams already use. IDLE STRESS HAIRLINE: while the panel has zero seams (nothing confirmed yet), it isn't a still frame — every few seconds the same buildSeam geometry engine draws a faint trunk-only hairline in --border at ~42% opacity (no branch, no --foreground, no drop-shadow highlight, so it can never be mistaken for a finished or in-progress real seam), holds briefly, dissolves, and a new one starts elsewhere at random after a pause. It measures the same data-seam-avoid exclusion rects as a real crack, is fully independent of sessionStorage (nothing about it persists), stops the instant a first real seam exists (the panel has its own trace by then and doesn't need the hint), and prefers-reduced-motion disables it outright. Props: actionLabel, pendingLabel, confirmedMessage, description, onConfirm, storageKey, maxSeams, resetKey, className."
  },
  "type": "registry:ui"
}