{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "success-nucleation",
  "title": "Success Nucleation",
  "description": "A payment/deploy/publish success moment built on supercooling: pending is a canvas particle field in faint restless Brownian shimmer, and the instant it resolves a nucleation flash fires at the exact point pressed and dendritic crystal growth races outward, freezing the shimmer solid in ~700ms under a now-calm confirmation line.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/success-nucleation/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// SeedCrystal — a high-stakes success moment built on supercooling. While an\n// action is pending (payment confirming, deploy running, publish in flight)\n// the panel behind the content is a supercooled liquid: a canvas particle\n// field of ~800 points doing damped random walks, a faint restless shimmer\n// that visibly holds more energy than a settled surface should. The instant\n// the pending action resolves, a nucleation flash fires at the exact point\n// the user pressed and dendritic crystal growth races outward from it —\n// needle segments advancing with side branches forking at ~60deg at\n// depth-decaying probability — while nearby shimmer particles lock onto the\n// nearest needle as its front passes, freezing the whole pane solid in\n// ~700ms. The confirmation line then sits calm on the now-still crystal.\n//\n// MECHANISM: growth is precomputed synchronously the instant success fires\n// (mulberry32-seeded), not simulated tip-by-tip per frame — every needle\n// segment carries a \"birth\" time (seconds from growth start) baked in when\n// it's built, the same reveal-by-threshold technique as switch-frost.\n// Each animation frame just draws every segment whose birth <= elapsed and\n// every particle whose precomputed lockBirth <= elapsed, which is O(live\n// geometry) per frame instead of O(particles x segments) per frame — the\n// nearest-segment lock search itself is a single one-time scan done once at\n// growth start, not repeated on every tick.\n//\n// A locked particle's target point and birth are baked in at that same scan;\n// past its lockBirth it eases from its ambient position onto the needle\n// over a short settle window, tinting from --ns-muted toward --foreground as it\n// commits. Once every particle has finished settling the loop cancels itself\n// entirely — \"frozen\" is a real stopped rAF, not a slow one, matching the\n// brief's \"freezing solid\" rather than an animation that merely looks still.\n//\n// TOKENS: every drawn color is read from --ns-muted / --foreground via\n// getComputedStyle at mount and re-derived on a MutationObserver watching\n// documentElement class changes (both the live loop and a frozen redraw use\n// the same ref, so a theme flip repaints correctly whether mid-growth or\n// long settled). The canvas itself is never painted with a background —\n// --background shows through from the panel div underneath it.\n//\n// A11Y: canvas is aria-hidden + role=presentation. Success is announced via\n// a role=status aria-live=assertive line that mounts the instant React state\n// flips to \"success\" — independent of the ~700ms canvas animation, which is\n// purely decorative. The trigger button is never disabled (state re-entry is\n// guarded in JS, not via the disabled attribute), so it stays clickable\n// throughout and never blocks anything. Reduced motion swaps the whole\n// canvas for one static, seeded-once crystal texture — no shimmer, no\n// growth — with the same status text driven by the same state machine.\n// ---------------------------------------------------------------------------\n\ntype Status = \"idle\" | \"pending\" | \"success\";\ntype RGB = [number, number, number];\n\ninterface Seg {\n  x1: number;\n  y1: number;\n  x2: number;\n  y2: number;\n  depth: number;\n  birth: number; // seconds from growth start\n}\n\ninterface Particle {\n  bx: number; // base x, px\n  by: number; // base y, px\n  dx: number; // OU shimmer offset x, px\n  dy: number;\n  vx: number;\n  vy: number;\n  lockBirth: number; // seconds; Infinity = never locks\n  lx: number; // lock target x\n  ly: number; // lock target y\n}\n\nconst PARTICLE_COUNT = 800;\nconst MAX_SEGS = 900;\nconst LOCK_RADIUS = 15; // px\nconst SETTLE_S = 0.22;\nconst FLASH_MS = 180;\nconst FREEZE_PAD_MS = 260; // extra time after growMs for the last lock to settle\n\nfunction clamp01(v: number) {\n  return Math.min(1, Math.max(0, v));\n}\nfunction lerp(a: number, b: number, t: number) {\n  return a + (b - a) * t;\n}\nfunction easeOutCubic(t: number) {\n  const u = 1 - t;\n  return 1 - u * u * u;\n}\n\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction parseHex(v: string): RGB | null {\n  const m = v.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);\n  if (!m) return null;\n  let h = m[1]!;\n  if (h.length === 3) h = h.split(\"\").map((c) => c + c).join(\"\");\n  const n = parseInt(h, 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction readToken(el: HTMLElement, name: string, fallback: RGB): RGB {\n  return parseHex(getComputedStyle(el).getPropertyValue(name)) ?? fallback;\n}\n\nfunction rgbaStr(rgb: RGB, a: number): string {\n  return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`;\n}\n\nfunction mixRgb(a: RGB, b: RGB, t: number): RGB {\n  return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)];\n}\n\nfunction buildDust(count: number, w: number, h: number, rand: () => number): Particle[] {\n  const out: Particle[] = [];\n  for (let i = 0; i < count; i++) {\n    out.push({\n      bx: lerp(w * 0.04, w * 0.96, rand()),\n      by: lerp(h * 0.04, h * 0.96, rand()),\n      dx: 0,\n      dy: 0,\n      vx: 0,\n      vy: 0,\n      lockBirth: Infinity,\n      lx: 0,\n      ly: 0,\n    });\n  }\n  return out;\n}\n\n// Precompute the whole dendritic pattern up front: primary arms radiate from\n// the nucleation point, each a slightly curved walk that throws side\n// branches at ~60deg with depth-decaying probability. Every segment's birth\n// time is baked in at build time (reveal-by-threshold at draw time), the\n// same trick switch-frost uses for its frost spines.\nfunction buildCrystal(\n  rand: () => number,\n  cx: number,\n  cy: number,\n  w: number,\n  h: number,\n  growS: number\n): { segs: Seg[]; maxBirth: number } {\n  const segs: Seg[] = [];\n  const STEP = 4;\n  const maxLen = Math.hypot(w, h) * 0.58;\n  const speed = maxLen / growS; // px/s, tuned so a straight arm finishes near growS\n  const stepDur = STEP / speed;\n  const maxDepth = 3;\n  const baseProb = 0.5;\n  const decay = 0.5;\n\n  function grow(x: number, y: number, angle: number, depth: number, birth: number, steps: number) {\n    let a = angle;\n    let bx = x;\n    let by = y;\n    let b = birth;\n    const curv = (rand() - 0.5) * 0.05;\n    for (let k = 0; k < steps && segs.length < MAX_SEGS; k++) {\n      a += curv + (rand() - 0.5) * 0.14;\n      const nx = bx + Math.cos(a) * STEP;\n      const ny = by + Math.sin(a) * STEP;\n      if (nx < 1 || nx > w - 1 || ny < 1 || ny > h - 1) break;\n      const birthClamped = Math.min(b, growS);\n      segs.push({ x1: bx, y1: by, x2: nx, y2: ny, depth, birth: birthClamped });\n      if (depth < maxDepth && k > 1 && k % 3 === 0) {\n        const p = baseProb * Math.pow(decay, depth);\n        if (rand() < p) {\n          const side = rand() < 0.5 ? 1 : -1;\n          const branchAngle = a + side * (0.96 + rand() * 0.18); // ~55-65deg\n          const branchSteps = Math.max(3, Math.floor(steps * 0.45));\n          grow(nx, ny, branchAngle, depth + 1, b + stepDur * 0.6, branchSteps);\n        }\n      }\n      b += stepDur;\n      bx = nx;\n      by = ny;\n    }\n  }\n\n  const arms = 5 + Math.floor(rand() * 2);\n  const spread = (Math.PI * 2) / arms;\n  for (let i = 0; i < arms; i++) {\n    const angle = i * spread + (rand() - 0.5) * spread * 0.6;\n    grow(cx, cy, angle, 0, 0, Math.ceil(maxLen / STEP));\n  }\n\n  let maxBirth = 0;\n  for (const s of segs) if (s.birth > maxBirth) maxBirth = s.birth;\n  return { segs, maxBirth: Math.max(maxBirth, 0.001) };\n}\n\n// one-time nearest-segment-endpoint scan: every particle either finds a\n// needle endpoint within LOCK_RADIUS (and adopts that segment's birth as its\n// own lockBirth + that endpoint as its settle target) or never locks and\n// simply stops moving, in place, when the pane freezes.\nfunction assignLocks(particles: Particle[], segs: Seg[]) {\n  for (const p of particles) {\n    let bestD = LOCK_RADIUS * LOCK_RADIUS;\n    let bestSeg: Seg | null = null;\n    for (const s of segs) {\n      const dx = s.x2 - p.bx;\n      const dy = s.y2 - p.by;\n      const d = dx * dx + dy * dy;\n      if (d < bestD) {\n        bestD = d;\n        bestSeg = s;\n      }\n    }\n    if (bestSeg) {\n      p.lockBirth = bestSeg.birth;\n      p.lx = bestSeg.x2;\n      p.ly = bestSeg.y2;\n    }\n  }\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\nexport interface SeedCrystalProps {\n  /** idle button label. default \"Confirm payment\" */\n  label?: string;\n  /** button label while the action is in flight. default \"Confirming…\" */\n  pendingLabel?: string;\n  /** the calm payoff line, also the aria-live=assertive announcement text. default \"Payment confirmed\" */\n  confirmedText?: string;\n  /** called on trigger; return a Promise and success plays on resolve (or reverts to idle on reject) */\n  onConfirm?: () => void | Promise<void>;\n  /** fallback simulated pending duration when onConfirm is absent or synchronous. default 900 */\n  pendingMs?: number;\n  /** crystal growth duration in ms. default 700 */\n  growMs?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function SeedCrystal({\n  label = \"Confirm payment\",\n  pendingLabel = \"Confirming…\",\n  confirmedText = \"Payment confirmed\",\n  onConfirm,\n  pendingMs = 900,\n  growMs = 700,\n  className = \"\",\n}: SeedCrystalProps) {\n  const reduced = useReducedMotion();\n  const [status, setStatus] = useState<Status>(\"idle\");\n  const statusRef = useRef<Status>(\"idle\");\n  statusRef.current = status;\n\n  const panelRef = useRef<HTMLDivElement>(null);\n  const btnRef = useRef<HTMLButtonElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  const pendingMsRef = useRef(pendingMs);\n  pendingMsRef.current = pendingMs;\n  const growMsRef = useRef(growMs);\n  growMsRef.current = growMs;\n  const onConfirmRef = useRef(onConfirm);\n  onConfirmRef.current = onConfirm;\n\n  const engineRef = useRef({\n    w: 0,\n    h: 0,\n    dpr: 1,\n    particles: [] as Particle[],\n    segs: [] as Seg[],\n    maxBirth: 0.001,\n    growStart: 0, // performance.now() ms\n    nucX: 0,\n    nucY: 0,\n    frozen: false,\n    raf: 0,\n    lastNow: 0,\n    visible: true,\n    muted: [141, 141, 141] as RGB,\n    foreground: [237, 237, 237] as RGB,\n    reduced: false,\n    seedCounter: 1,\n  });\n\n  const controls = useRef<{\n    startPending?: () => void;\n    startGrowth?: (x: number, y: number) => void;\n  }>({});\n\n  useEffect(() => {\n    const panel = panelRef.current;\n    const canvas = canvasRef.current;\n    if (!panel || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const e = engineRef.current;\n\n    const syncColors = () => {\n      e.muted = readToken(panel, \"--ns-muted\", e.muted);\n      e.foreground = readToken(panel, \"--foreground\", e.foreground);\n    };\n\n    const resize = () => {\n      const r = panel.getBoundingClientRect();\n      e.w = Math.max(1, r.width);\n      e.h = Math.max(1, r.height);\n      e.dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.round(e.w * e.dpr);\n      canvas.height = Math.round(e.h * e.dpr);\n      canvas.style.width = `${e.w}px`;\n      canvas.style.height = `${e.h}px`;\n      if (statusRef.current === \"idle\" || statusRef.current === \"pending\") {\n        // ambient only — safe to regenerate on resize, nothing depends on continuity\n        e.particles = buildDust(PARTICLE_COUNT, e.w, e.h, mulberry32(e.seedCounter * 7919 + 1));\n      }\n    };\n\n    const drawStatic = () => {\n      // reduced motion: one faint, seeded-once dendritic texture, no motion.\n      ctx.setTransform(e.dpr, 0, 0, e.dpr, 0, 0);\n      ctx.clearRect(0, 0, e.w, e.h);\n      const rand = mulberry32(424242);\n      const { segs } = buildCrystal(rand, e.w * 0.5, e.h * 0.5, e.w, e.h, 1);\n      ctx.lineCap = \"round\";\n      ctx.strokeStyle = rgbaStr(e.foreground, 0.16);\n      ctx.lineWidth = 0.75;\n      ctx.beginPath();\n      for (const s of segs) {\n        ctx.moveTo(s.x1, s.y1);\n        ctx.lineTo(s.x2, s.y2);\n      }\n      ctx.stroke();\n    };\n\n    const drawDust = () => {\n      ctx.setTransform(e.dpr, 0, 0, e.dpr, 0, 0);\n      ctx.clearRect(0, 0, e.w, e.h);\n      ctx.fillStyle = rgbaStr(e.muted, 0.55);\n      for (const p of e.particles) {\n        ctx.beginPath();\n        ctx.arc(p.bx + p.dx, p.by + p.dy, 0.9, 0, Math.PI * 2);\n        ctx.fill();\n      }\n    };\n\n    const stepDust = (dtS: number) => {\n      const K = 46; // spring back toward origin\n      const C = 7.5; // damping\n      const NOISE = 90;\n      for (const p of e.particles) {\n        p.vx += (-K * p.dx - C * p.vx) * dtS + (Math.random() - 0.5) * NOISE * dtS;\n        p.vy += (-K * p.dy - C * p.vy) * dtS + (Math.random() - 0.5) * NOISE * dtS;\n        p.dx += p.vx * dtS;\n        p.dy += p.vy * dtS;\n      }\n    };\n\n    const drawGrowth = (elapsedMs: number) => {\n      ctx.setTransform(e.dpr, 0, 0, e.dpr, 0, 0);\n      ctx.clearRect(0, 0, e.w, e.h);\n      const elapsedS = elapsedMs / 1000;\n\n      // nucleation flash\n      if (elapsedMs < FLASH_MS) {\n        const t = elapsedMs / FLASH_MS;\n        const r = lerp(2, 46, easeOutCubic(t));\n        const a = (1 - t) * 0.85;\n        const grad = ctx.createRadialGradient(e.nucX, e.nucY, 0, e.nucX, e.nucY, r);\n        grad.addColorStop(0, rgbaStr(e.foreground, a));\n        grad.addColorStop(1, rgbaStr(e.foreground, 0));\n        ctx.fillStyle = grad;\n        ctx.fillRect(0, 0, e.w, e.h);\n      }\n\n      // particles: ambient shimmer until their needle passes, then settle\n      // onto it, tinting from --ns-muted toward --foreground as they commit.\n      // (velocity/offset integration for the still-unlocked ones happens once\n      // per frame in the caller, before this draw — this only reads state.)\n      for (const p of e.particles) {\n        if (elapsedS < p.lockBirth) {\n          ctx.fillStyle = rgbaStr(e.muted, 0.55);\n          ctx.beginPath();\n          ctx.arc(p.bx + p.dx, p.by + p.dy, 0.9, 0, Math.PI * 2);\n          ctx.fill();\n        } else {\n          const t = clamp01((elapsedS - p.lockBirth) / SETTLE_S);\n          const ease = easeOutCubic(t);\n          const x = lerp(p.bx + p.dx, p.lx, ease);\n          const y = lerp(p.by + p.dy, p.ly, ease);\n          const col = mixRgb(e.muted, e.foreground, ease);\n          ctx.fillStyle = rgbaStr(col, 0.5 + 0.45 * ease);\n          ctx.beginPath();\n          ctx.arc(x, y, 0.9 + 0.5 * ease, 0, Math.PI * 2);\n          ctx.fill();\n        }\n      }\n\n      // needles: soft halo pass then a crisp core, revealed by birth <= elapsed\n      ctx.lineCap = \"round\";\n      const widths = [1.4, 1.0, 0.75, 0.6];\n      for (let pass = 0; pass < 2; pass++) {\n        for (let d = 0; d <= 3; d++) {\n          let any = false;\n          ctx.beginPath();\n          for (const s of e.segs) {\n            if (s.depth !== d || s.birth > elapsedS) continue;\n            ctx.moveTo(s.x1, s.y1);\n            ctx.lineTo(s.x2, s.y2);\n            any = true;\n          }\n          if (!any) continue;\n          if (pass === 0) {\n            ctx.strokeStyle = rgbaStr(e.foreground, 0.16);\n            ctx.lineWidth = widths[d]! + 2.2;\n          } else {\n            ctx.strokeStyle = rgbaStr(e.foreground, 0.92);\n            ctx.lineWidth = widths[d]!;\n          }\n          ctx.stroke();\n        }\n      }\n    };\n\n    // idle draws nothing (the panel's own --background shows through) and\n    // does not reschedule itself — there's nothing to animate at rest, so\n    // the loop stops rather than clearing an empty canvas 60x/s forever.\n    const drawIdle = () => {\n      ctx.setTransform(e.dpr, 0, 0, e.dpr, 0, 0);\n      ctx.clearRect(0, 0, e.w, e.h);\n    };\n\n    const paintStill = () => {\n      if (e.reduced) return drawStatic();\n      const st = statusRef.current;\n      if (st === \"success\") drawGrowth(e.frozen ? growMsRef.current + FREEZE_PAD_MS : performance.now() - e.growStart);\n      else if (st === \"pending\") drawDust();\n      else drawIdle();\n    };\n\n    const loop = (now: number) => {\n      const dtMs = Math.min(48, now - e.lastNow);\n      e.lastNow = now;\n      const st = statusRef.current;\n\n      if (st === \"success\") {\n        const elapsedMs = now - e.growStart;\n        // advance shimmer physics only for particles still unlocked\n        const dtS = dtMs / 1000;\n        const elapsedS = elapsedMs / 1000;\n        const K = 46;\n        const C = 7.5;\n        const NOISE = 90;\n        for (const p of e.particles) {\n          if (elapsedS < p.lockBirth) {\n            p.vx += (-K * p.dx - C * p.vx) * dtS + (Math.random() - 0.5) * NOISE * dtS;\n            p.vy += (-K * p.dy - C * p.vy) * dtS + (Math.random() - 0.5) * NOISE * dtS;\n            p.dx += p.vx * dtS;\n            p.dy += p.vy * dtS;\n          }\n        }\n        drawGrowth(elapsedMs);\n        if (elapsedMs >= growMsRef.current + FREEZE_PAD_MS) {\n          drawGrowth(growMsRef.current + FREEZE_PAD_MS); // final settled frame\n          e.frozen = true;\n          e.raf = 0;\n          return; // loop stops — genuinely frozen, not just slow\n        }\n      } else if (st === \"pending\") {\n        stepDust(dtMs / 1000);\n        drawDust();\n      } else {\n        drawIdle();\n        e.raf = 0;\n        return; // nothing to animate at rest\n      }\n\n      e.raf = e.visible ? requestAnimationFrame(loop) : 0;\n    };\n\n    const wake = () => {\n      if (e.raf || !e.visible || e.frozen) return;\n      e.lastNow = performance.now();\n      e.raf = requestAnimationFrame(loop);\n    };\n\n    controls.current.startPending = () => {\n      e.frozen = false;\n      e.particles = buildDust(PARTICLE_COUNT, e.w, e.h, mulberry32(e.seedCounter * 104729 + 3));\n      wake();\n    };\n\n    controls.current.startGrowth = (nx: number, ny: number) => {\n      e.nucX = nx;\n      e.nucY = ny;\n      e.growStart = performance.now();\n      const seed = (e.seedCounter++ * 2654435761 + Math.floor(nx) * 97 + Math.floor(ny)) >>> 0;\n      const rand = mulberry32(seed || 1);\n      const growS = growMsRef.current / 1000;\n      const built = buildCrystal(rand, nx, ny, e.w, e.h, growS);\n      e.segs = built.segs;\n      e.maxBirth = built.maxBirth;\n      assignLocks(e.particles, e.segs);\n      e.frozen = false;\n      wake();\n    };\n\n    resize();\n    syncColors();\n    e.reduced = reduced;\n    paintStill();\n\n    const ro = new ResizeObserver(() => {\n      resize();\n      paintStill();\n    });\n    ro.observe(panel);\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        e.visible = !!entry?.isIntersecting;\n        if (e.visible) wake();\n        else if (e.raf) {\n          cancelAnimationFrame(e.raf);\n          e.raf = 0;\n        }\n      },\n      { threshold: 0 }\n    );\n    io.observe(panel);\n\n    const mo = new MutationObserver(() => {\n      syncColors();\n      if (!e.raf) paintStill(); // repaint the current still frame with new colors\n    });\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    return () => {\n      cancelAnimationFrame(e.raf);\n      e.raf = 0;\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [reduced]);\n\n  function handleActivate(clientX: number, clientY: number) {\n    if (statusRef.current !== \"idle\") return;\n    setStatus(\"pending\");\n\n    const canvas = canvasRef.current;\n    const btn = btnRef.current;\n    if (!reduced) controls.current.startPending?.();\n\n    const finishToSuccess = () => {\n      setStatus(\"success\");\n      const r = canvas?.getBoundingClientRect();\n      let nx = engineRef.current.w * 0.5;\n      let ny = engineRef.current.h * 0.5;\n      // a real pointer click carries genuine coordinates; a keyboard-fired\n      // click reports (0,0) in every browser, so treat that as \"no point\"\n      // and nucleate from the button's own centre instead of the corner.\n      const bt = btn?.getBoundingClientRect();\n      const px = clientX === 0 && clientY === 0 && bt ? bt.left + bt.width / 2 : clientX;\n      const py = clientX === 0 && clientY === 0 && bt ? bt.top + bt.height / 2 : clientY;\n      if (r) {\n        nx = clamp01((px - r.left) / (r.width || 1)) * engineRef.current.w;\n        ny = clamp01((py - r.top) / (r.height || 1)) * engineRef.current.h;\n      }\n      if (!reduced) controls.current.startGrowth?.(nx, ny);\n    };\n\n    const ret = onConfirmRef.current?.();\n    if (ret && typeof (ret as Promise<void>).then === \"function\") {\n      (ret as Promise<void>).then(finishToSuccess, () => setStatus(\"idle\"));\n    } else {\n      window.setTimeout(finishToSuccess, pendingMsRef.current);\n    }\n  }\n\n  return (\n    <div className={className}>\n      <style>{`\n@keyframes ns-seed-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n.ns-seed-in{animation:ns-seed-in 320ms cubic-bezier(0.16,1,0.3,1) both}\n@media (prefers-reduced-motion: reduce){ .ns-seed-in{animation:none} }\n`}</style>\n\n      <div\n        ref={panelRef}\n        data-seed-panel\n        className=\"relative w-full overflow-hidden rounded-md border border-border bg-background p-6\"\n        style={{ minHeight: 220 }}\n      >\n        <canvas\n          ref={canvasRef}\n          aria-hidden\n          role=\"presentation\"\n          className=\"pointer-events-none absolute inset-0 h-full w-full\"\n        />\n\n        <div className=\"relative z-10 flex h-full flex-col justify-between gap-6\">\n          <div>\n            <p className=\"font-mono text-xs uppercase tracking-[0.2em] text-ns-muted\">\n              Secure checkout\n            </p>\n            <p className=\"mt-1 text-sm text-ns-muted\">\n              One-time confirmation — held in a supercooled state until you act.\n            </p>\n          </div>\n\n          <div className=\"flex flex-col items-start gap-4\">\n            <button\n              ref={btnRef}\n              type=\"button\"\n              onClick={(ev) => handleActivate(ev.clientX, ev.clientY)}\n              aria-busy={status === \"pending\"}\n              className=\"inline-flex items-center gap-2 rounded-sm bg-ns-accent px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-ns-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n            >\n              {status === \"success\" ? (\n                <>\n                  <svg viewBox=\"0 0 16 16\" className=\"h-3.5 w-3.5\" fill=\"none\" aria-hidden>\n                    <path\n                      d=\"M3.5 8.5l3 3 6-7\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"1.6\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                    />\n                  </svg>\n                  Confirmed\n                </>\n              ) : status === \"pending\" ? (\n                pendingLabel\n              ) : (\n                label\n              )}\n            </button>\n\n            {status === \"success\" ? (\n              <p\n                role=\"status\"\n                aria-live=\"assertive\"\n                data-seed-status\n                className=\"ns-seed-in text-sm font-medium text-foreground\"\n              >\n                {confirmedText}\n              </p>\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/success-nucleation.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-ns-accent-hover": "var(--ns-accent-hover)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "ns-accent-hover": "#0059d1"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "confirmation",
      "success",
      "payment",
      "canvas",
      "particles",
      "phase-change",
      "micro-interaction",
      "aria-live"
    ],
    "instruction": "A self-contained success-moment card: a canvas layer sits behind a small panel's content, transparent over --background, and a real <button> (default label 'Confirm payment') drives a three-state machine — idle, pending, success — that is never re-enterable once past idle (a status guard, not a disabled attribute, so the button stays clickable and focusable throughout and never blocks anything). Clicking calls the optional onConfirm callback; if it returns a Promise, the pending shimmer holds until that Promise settles (success on resolve, a silent revert to idle on reject), otherwise a fallback pendingMs (default 900) simulates the wait — this is how a consumer wires a real payment/deploy/publish call into the moment. PENDING: ~800 points scattered across the panel each do a damped Ornstein-Uhlenbeck random walk (spring pulling back toward its own origin, velocity damping, small continuous noise) around a roughly 1px amplitude, drawn as sub-pixel dots in --ns-muted — a faint, restless shimmer that reads as holding more energy than a settled surface should. SUCCESS: the moment the pending action resolves, React state flips to 'success' synchronously (independent of anything canvas-side) and a role=status aria-live=assertive line mounts immediately announcing confirmedText (default 'Payment confirmed') — the announcement is never gated behind the animation. On the canvas, the exact clientX/clientY of the triggering click (or, for a keyboard-fired click reporting (0,0), the button's own centre) becomes the nucleation point: a radial flash blooms there over ~180ms in --foreground, and a whole dendritic pattern — 5-6 primary arms radiating outward with gentle per-step curvature, throwing side branches at a randomized ~55-65deg with depth-decaying probability (mulberry32-seeded fresh per success) — is precomputed synchronously as a flat list of needle segments, each carrying a baked-in birth time. Every frame from then on simply draws every segment whose birth has passed, in --foreground, tapering by branch depth; the whole pattern finishes within growMs (default 700). In the same one-time build, every ambient particle is matched to its nearest needle endpoint (within a small lock radius) and inherits that segment's birth as its own lock time; a particle whose crystal front hasn't reached it keeps shimmering right up until it does, then eases onto the needle over a short spring-eased settle window, its color interpolating from --ns-muted to --foreground as it commits — the visible read of shimmer 'locking solid' as the front passes. A particle the crystal never reaches simply stops moving in place once the pane freezes. Once every particle has finished settling the animation loop cancels itself outright — a real stopped rAF, not merely a slow one — and a MutationObserver watching documentElement class changes repaints that exact frozen frame with fresh colors on every theme flip, whether mid-growth or long settled. The confirmation line fades in gently (a 320ms entrance) below the button, calm on the now-still crystal. Colors are read once at mount via getComputedStyle(--ns-muted, --foreground) and re-derived on the same MutationObserver; the canvas paints no background of its own. Reduced motion swaps the entire canvas for one static, seeded-once dendritic texture at low opacity — no shimmer, no growth, the same status text and state machine, just no motion. IntersectionObserver pauses the rAF loop offscreen and ResizeObserver keeps the backing store correct; canvas is aria-hidden + role=presentation throughout. Props: label, pendingLabel, confirmedText, onConfirm, pendingMs, growMs, className."
  },
  "type": "registry:ui"
}