{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dew-coalesce",
  "title": "Dew Coalesce",
  "description": "An empty state as condensation on cold glass: a PRNG-seeded droplet field nucleates, grows, jitters and merges, and a droplet crossing the runoff threshold streaks down and wipes a real clean track through a masked fog veil.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/dew-coalesce/component.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// DewCoalesce — an empty state as condensation on cold glass, not an\n// illustration standing in for absence. A field of droplets (plain SVG\n// circles stroked in --border) nucleates across the panel from a seeded\n// PRNG, grows imperceptibly on a slow tick, jitters, and merges with a\n// close neighbour into one droplet of the combined area plus a brief settle\n// wobble. The instant a droplet's radius crosses the runoff threshold it\n// stops growing, streaks down along a slightly meandering path, and that\n// exact path becomes a rounded-pill hole cut into an SVG mask — the fog\n// (a low-opacity --ns-muted veil painted through that mask) is genuinely\n// wiped where the droplet ran, then slowly re-fogs. A second, permanent\n// hole stays cut around the heading/body/CTA block, so the copy is legible\n// because the glass was cleared there, not merely because it happens to sit\n// on top in z-order.\n//\n// All physics is driven by a single setInterval tick (no rAF, no canvas):\n// each tick is a pure function over the previous droplet array, and CSS\n// transitions on cx/cy/r smooth the sub-pixel deltas between ticks into\n// visible, organic motion. Runoff and track-fade are handled by two small\n// per-item subcomponents (RunoffDroplet, TrackRect) using the same\n// mount-then-flip-state \"settle\" idiom already used elsewhere in this\n// registry (see empty-state-pegboard's Slot) rather than manual DOM writes.\n//\n// Distinct from empty-state-sonar: sonar actively pings outward from a\n// center and is interrupted the instant data arrives — a probing/loading\n// state machine. dew-coalesce has no data-arrival handoff at all; it is a\n// permanently ambient decorative background for a static empty state, and\n// its one event (runoff) is a quiet reveal, not a signal.\n// ---------------------------------------------------------------------------\n\nexport interface DewCoalesceProps {\n  /** Headline — what is missing. */\n  title?: string;\n  /** One line of supporting copy. */\n  description?: string;\n  /** Label of the single accent CTA. Pass an empty string to omit the button. */\n  actionLabel?: string;\n  onAction?: () => void;\n  /** Optional mark above the headline — an icon, a glyph, anything small. */\n  icon?: ReactNode;\n  /** Seeds the initial droplet field so the panel's starting arrangement is reproducible. */\n  seed?: number;\n  className?: string;\n}\n\ninterface Rect {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\ntype DropletState = \"grow\" | \"settle\" | \"runoff\";\n\ninterface Droplet {\n  id: number;\n  x: number;\n  y: number;\n  r: number;\n  state: DropletState;\n  settleTicks: number;\n  runDX?: number;\n  runDY?: number;\n  runMs?: number;\n}\n\ninterface TrackSpec {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  rot: number;\n}\n\ninterface Track extends TrackSpec {\n  id: number;\n}\n\nconst TICK_MS = 380;\nconst RUNOFF_R = 7.6;\nconst MERGE_FACTOR = 0.85;\nconst GROW_MIN = 0.05;\nconst GROW_MAX = 0.16;\nconst JITTER_IDLE = 0.15;\nconst JITTER_SETTLE = 0.6;\nconst SETTLE_TICKS = 3;\nconst MAX_TRACKS = 6;\nconst TRACK_GROW_MS = 900;\nconst TRACK_HOLD_MS = 2600;\nconst TRACK_REFOG_MS = 5200;\n// Padding around the measured copy block that stays permanently clear of fog.\n// Kept tight: with the wide feather above, a large pad cleared essentially the\n// whole panel and left the veil visible only as a band along the bottom edge.\nconst PATCH_PAD = 6;\n\nconst CSS = `\n.ns-dew-drop{fill:none;stroke:var(--border);stroke-width:1;vector-effect:non-scaling-stroke;transition:cx 340ms ease-out,cy 340ms ease-out,r 340ms ease-out}\n.ns-dew-runoff{fill:none;stroke:var(--border);stroke-width:1;vector-effect:non-scaling-stroke;animation-name:ns-dew-runoff;animation-timing-function:cubic-bezier(.4,0,.2,1);animation-fill-mode:forwards;transform-box:fill-box}\n@keyframes ns-dew-runoff{\n  0%{transform:translate(0,0);opacity:.85}\n  35%{transform:translate(calc(var(--dew-dx) * .5),calc(var(--dew-dy) * .4))}\n  70%{transform:translate(calc(var(--dew-dx) * -.25),calc(var(--dew-dy) * .78))}\n  100%{transform:translate(0,var(--dew-dy));opacity:0}\n}\n@media (prefers-reduced-motion: reduce){\n  .ns-dew-drop{transition:none}\n  .ns-dew-runoff{animation:none}\n}\n`;\n\nfunction mulberry32(seed: number): () => number {\n  let a = seed >>> 0 || 1;\n  return function rng() {\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 clamp(v: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, v));\n}\n\nfunction inRect(x: number, y: number, r: Rect | null, pad: number): boolean {\n  if (!r) return false;\n  return x > r.x - pad && x < r.x + r.w + pad && y > r.y - pad && y < r.y + r.h + pad;\n}\n\nfunction spawnOne(rng: () => number, w: number, h: number, avoid: Rect | null, id: number): Droplet {\n  let x = w / 2;\n  let y = h / 2;\n  for (let tries = 0; tries < 6; tries++) {\n    x = 6 + rng() * Math.max(1, w - 12);\n    y = 6 + rng() * Math.max(1, h - 12);\n    if (!inRect(x, y, avoid, 12)) break;\n  }\n  return { id, x, y, r: 1 + rng() * 2.2, state: \"grow\", settleTicks: 0 };\n}\n\n// Pure over the previous frame: grow/jitter every non-runoff droplet, merge\n// close pairs into one (combined area, settle wobble), promote any droplet\n// that crossed the runoff threshold, and occasionally nucleate a\n// replacement toward the field's target count. No side effects — the\n// caller decides what to do with the returned tracks.\nfunction tickPhysics(\n  prev: Droplet[],\n  rng: () => number,\n  w: number,\n  h: number,\n  targetCount: number,\n  avoid: Rect | null,\n  nextId: () => number\n): { droplets: Droplet[]; newTracks: TrackSpec[] } {\n  const arr: Droplet[] = prev.map((d) => {\n    if (d.state === \"runoff\") return d;\n    const jitter = d.state === \"settle\" ? JITTER_SETTLE : JITTER_IDLE;\n    const x = clamp(d.x + (rng() - 0.5) * jitter, 4, Math.max(4, w - 4));\n    const y = clamp(d.y + (rng() - 0.5) * jitter, 4, Math.max(4, h - 4));\n    const r = d.r + GROW_MIN + rng() * (GROW_MAX - GROW_MIN);\n    let settleTicks = d.settleTicks;\n    let state = d.state;\n    if (state === \"settle\") {\n      settleTicks -= 1;\n      if (settleTicks <= 0) state = \"grow\";\n    }\n    return { ...d, x, y, r, state, settleTicks };\n  });\n\n  // proximity merge — one pass, at most one merge per droplet per tick\n  for (let i = 0; i < arr.length; i++) {\n    const a = arr[i]!;\n    if (a.state === \"runoff\") continue;\n    for (let j = i + 1; j < arr.length; j++) {\n      const b = arr[j]!;\n      if (b.state === \"runoff\") continue;\n      const dist = Math.hypot(a.x - b.x, a.y - b.y);\n      if (dist < (a.r + b.r) * MERGE_FACTOR) {\n        const areaSum = a.r * a.r + b.r * b.r;\n        const nr = Math.sqrt(areaSum);\n        const nx = (a.x * a.r * a.r + b.x * b.r * b.r) / areaSum;\n        const ny = (a.y * a.r * a.r + b.y * b.r * b.r) / areaSum;\n        arr[i] = { ...a, x: nx, y: ny, r: nr, state: \"settle\", settleTicks: SETTLE_TICKS };\n        arr.splice(j, 1);\n        break;\n      }\n    }\n  }\n\n  const newTracks: TrackSpec[] = [];\n  for (let i = 0; i < arr.length; i++) {\n    const d = arr[i]!;\n    if (d.state === \"runoff\" || d.r < RUNOFF_R) continue;\n    const dy = Math.max(50, h - d.y) * (0.55 + rng() * 0.35);\n    const dx = (rng() - 0.5) * 2 * (14 + rng() * 12);\n    const ms = 850 + rng() * 480;\n    newTracks.push({ x: d.x, y: d.y, width: d.r * 2.4, height: dy, rot: clamp(dx * 0.4, -14, 14) });\n    arr[i] = { ...d, state: \"runoff\", runDX: dx, runDY: dy, runMs: ms };\n  }\n\n  const activeCount = arr.reduce((n, d) => (d.state === \"runoff\" ? n : n + 1), 0);\n  if (activeCount < targetCount && rng() < 0.6) {\n    arr.push(spawnOne(rng, w, h, avoid, nextId()));\n  }\n\n  return { droplets: arr, newTracks };\n}\n\nfunction RunoffDroplet({ d, onDone }: { d: Droplet; onDone: (id: number) => void }) {\n  return (\n    <circle\n      className=\"ns-dew-runoff\"\n      cx={d.x}\n      cy={d.y}\n      r={d.r}\n      style={\n        {\n          \"--dew-dx\": `${d.runDX ?? 0}px`,\n          \"--dew-dy\": `${d.runDY ?? 0}px`,\n          animationDuration: `${d.runMs ?? 1000}ms`,\n        } as CSSProperties\n      }\n      onAnimationEnd={() => onDone(d.id)}\n    />\n  );\n}\n\n// Grows from height 0 to its target on mount (the same double-rAF\n// mount-then-flip idiom empty-state-pegboard's Slot uses to get a real CSS\n// transition on first paint), holds, then fades back to fog — dew re-forms.\nfunction TrackRect({ track, onFaded }: { track: Track; onFaded: (id: number) => void }) {\n  const [grown, setGrown] = useState(false);\n  const [fading, setFading] = useState(false);\n\n  useEffect(() => {\n    let raf2 = 0;\n    const raf1 = requestAnimationFrame(() => {\n      raf2 = requestAnimationFrame(() => setGrown(true));\n    });\n    return () => {\n      cancelAnimationFrame(raf1);\n      cancelAnimationFrame(raf2);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!grown) return;\n    const t = window.setTimeout(() => setFading(true), TRACK_HOLD_MS);\n    return () => window.clearTimeout(t);\n  }, [grown]);\n\n  return (\n    <rect\n      x={track.x - track.width / 2}\n      y={track.y}\n      width={track.width}\n      height={grown ? track.height : 0}\n      rx={track.width / 2}\n      fill=\"black\"\n      style={{\n        transformOrigin: `${track.x}px ${track.y}px`,\n        transform: `rotate(${track.rot}deg)`,\n        opacity: fading ? 0 : 1,\n        transition: `height ${TRACK_GROW_MS}ms cubic-bezier(.16,1,.3,1), opacity ${TRACK_REFOG_MS}ms ease-out`,\n      }}\n      onTransitionEnd={(e) => {\n        if (e.propertyName === \"opacity\" && fading) onFaded(track.id);\n      }}\n    />\n  );\n}\n\nexport function DewCoalesce({\n  title = \"Nothing here yet\",\n  description = \"Items you create will show up here.\",\n  actionLabel = \"Create your first item\",\n  onAction,\n  icon,\n  seed = 1337,\n  className = \"\",\n}: DewCoalesceProps) {\n  const uid = useId();\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const contentRef = useRef<HTMLDivElement | null>(null);\n  const contentRectRef = useRef<Rect | null>(null);\n  const dropletsRef = useRef<Droplet[]>([]);\n  const nextDropletId = useRef(0);\n  const nextTrackId = useRef(0);\n  const seededOnceRef = useRef(false);\n  const rngRef = useRef<(() => number) | null>(null);\n  if (rngRef.current === null) rngRef.current = mulberry32(seed);\n\n  const [reduced, setReduced] = useState(false);\n  const [size, setSize] = useState<{ w: number; h: number } | null>(null);\n  const [contentRect, setContentRect] = useState<Rect | null>(null);\n  const [droplets, setDroplets] = useState<Droplet[]>([]);\n  const [tracks, setTracks] = useState<Track[]>([]);\n\n  const targetCount = useMemo(\n    () => (size ? clamp(Math.round((size.w * size.h) / 5200), 10, 26) : 14),\n    [size]\n  );\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  const measure = useCallback(() => {\n    const root = rootRef.current;\n    const content = contentRef.current;\n    if (!root) return;\n    const r = root.getBoundingClientRect();\n    if (!r.width || !r.height) return;\n    const w = Math.round(r.width);\n    const h = Math.round(r.height);\n    setSize((prev) => (prev && prev.w === w && prev.h === h ? prev : { w, h }));\n    if (content) {\n      const c = content.getBoundingClientRect();\n      const rect: Rect = {\n        x: c.left - r.left - PATCH_PAD,\n        y: c.top - r.top - PATCH_PAD,\n        w: c.width + PATCH_PAD * 2,\n        h: c.height + PATCH_PAD * 2,\n      };\n      contentRectRef.current = rect;\n      setContentRect((prev) =>\n        prev && prev.x === rect.x && prev.y === rect.y && prev.w === rect.w && prev.h === rect.h ? prev : rect\n      );\n    }\n  }, []);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(root);\n    if (contentRef.current) ro.observe(contentRef.current);\n    return () => ro.disconnect();\n  }, [measure]);\n\n  // Seed the field once the panel has a real size; on later resizes, clamp\n  // existing droplets into the new bounds rather than regenerating them.\n  useEffect(() => {\n    if (!size) return;\n    const rng = rngRef.current!;\n    if (!seededOnceRef.current) {\n      seededOnceRef.current = true;\n      const count = reduced ? Math.min(9, targetCount) : targetCount;\n      const initial = Array.from({ length: count }, () =>\n        spawnOne(rng, size.w, size.h, contentRectRef.current, nextDropletId.current++)\n      );\n      dropletsRef.current = initial;\n      setDroplets(initial);\n      return;\n    }\n    const clamped = dropletsRef.current.map((d) => ({\n      ...d,\n      x: clamp(d.x, 4, Math.max(4, size.w - 4)),\n      y: clamp(d.y, 4, Math.max(4, size.h - 4)),\n    }));\n    dropletsRef.current = clamped;\n    setDroplets(clamped);\n    // rng is a stable ref and targetCount/reduced only matter at the very\n    // first seed above; re-running this for later size changes is the\n    // intended clamp path.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [size]);\n\n  // The physics tick: growth, jitter, merge, runoff. Off entirely under\n  // reduced motion, which keeps the once-seeded static field as-is.\n  useEffect(() => {\n    if (reduced || !size) return;\n    const rng = rngRef.current!;\n    const id = window.setInterval(() => {\n      const result = tickPhysics(\n        dropletsRef.current,\n        rng,\n        size.w,\n        size.h,\n        targetCount,\n        contentRectRef.current,\n        () => nextDropletId.current++\n      );\n      dropletsRef.current = result.droplets;\n      setDroplets(result.droplets);\n      if (result.newTracks.length) {\n        const withIds: Track[] = result.newTracks.map((t) => ({ ...t, id: nextTrackId.current++ }));\n        setTracks((prev) => {\n          const merged = [...prev, ...withIds];\n          return merged.length > MAX_TRACKS ? merged.slice(merged.length - MAX_TRACKS) : merged;\n        });\n      }\n    }, TICK_MS);\n    return () => window.clearInterval(id);\n  }, [reduced, size, targetCount]);\n\n  const handleRunoffDone = useCallback((id: number) => {\n    dropletsRef.current = dropletsRef.current.filter((d) => d.id !== id);\n    setDroplets(dropletsRef.current);\n  }, []);\n\n  const handleTrackFaded = useCallback((id: number) => {\n    setTracks((prev) => prev.filter((t) => t.id !== id));\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      className={[\"relative isolate overflow-hidden rounded-[16px] border border-border bg-background\", className].join(\n        \" \"\n      )}\n    >\n      <style>{CSS}</style>\n\n      {size ? (\n        <svg\n          aria-hidden=\"true\"\n          focusable=\"false\"\n          className=\"pointer-events-none absolute inset-0 h-full w-full\"\n          viewBox={`0 0 ${size.w} ${size.h}`}\n          preserveAspectRatio=\"xMidYMid slice\"\n        >\n          <defs>\n            <filter id={`${uid}-feather`} x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\">\n              {/*\n                Condensation has no straight edges. At stdDeviation 3.5 the\n                content patch resolved to a crisp horizontal boundary and the\n                veil survived only as a hard-edged grey band across the bottom\n                of the panel — it read as a broken gradient, not fogged glass.\n                A much wider feather turns the same hole into a soft vignette.\n              */}\n              <feGaussianBlur stdDeviation=\"16\" />\n            </filter>\n            <mask id={`${uid}-mask`} maskContentUnits=\"userSpaceOnUse\">\n              <rect x={0} y={0} width={size.w} height={size.h} fill=\"white\" />\n              {contentRect ? (\n                <rect\n                  x={contentRect.x}\n                  y={contentRect.y}\n                  width={Math.max(0, contentRect.w)}\n                  height={Math.max(0, contentRect.h)}\n                  rx={18}\n                  fill=\"black\"\n                  filter={`url(#${uid}-feather)`}\n                />\n              ) : null}\n              {!reduced ? tracks.map((t) => <TrackRect key={t.id} track={t} onFaded={handleTrackFaded} />) : null}\n            </mask>\n          </defs>\n\n          <rect\n            x={0}\n            y={0}\n            width={size.w}\n            height={size.h}\n            fill=\"var(--ns-muted)\"\n            opacity={0.5}\n            mask={`url(#${uid}-mask)`}\n          />\n\n          <g>\n            {droplets\n              .filter((d) => d.state !== \"runoff\")\n              .map((d) => (\n                <circle key={d.id} className=\"ns-dew-drop\" cx={d.x} cy={d.y} r={d.r} />\n              ))}\n            {droplets\n              .filter((d) => d.state === \"runoff\")\n              .map((d) => (\n                <RunoffDroplet key={d.id} d={d} onDone={handleRunoffDone} />\n              ))}\n          </g>\n        </svg>\n      ) : null}\n\n      <div ref={contentRef} className=\"relative flex flex-col items-center gap-1.5 px-8 py-14 text-center\">\n        {icon ? (\n          <div className=\"mb-3 flex h-11 w-11 items-center justify-center rounded-sm border border-border text-ns-muted\">\n            {icon}\n          </div>\n        ) : null}\n        <h2 className=\"text-base font-semibold tracking-tight text-foreground\">{title}</h2>\n        {description ? <p className=\"mt-1 max-w-[36ch] text-sm leading-relaxed text-ns-muted\">{description}</p> : null}\n        {actionLabel ? (\n          <button\n            type=\"button\"\n            onClick={onAction}\n            className=\"mt-5 inline-flex items-center justify-center rounded-sm bg-ns-accent px-4 py-2 text-sm font-medium text-white transition-[background-color,transform,box-shadow] duration-200 ease-out hover:-translate-y-px hover:bg-ns-accent-hover hover:shadow-[0_0_0_1px_var(--ns-accent)] active:translate-y-0 active:bg-ns-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            {actionLabel}\n          </button>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/dew-coalesce.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": "core",
    "tags": [
      "empty-state",
      "svg",
      "mask",
      "prng",
      "condensation",
      "ambient",
      "aria-hidden",
      "cta",
      "inbox",
      "search"
    ],
    "instruction": "A decorative empty-state panel, `<DewCoalesce title description actionLabel onAction icon seed className />`, styled as condensation on cold glass. On mount, once the panel's real pixel size is measured, a droplet field is nucleated from a seeded PRNG (mulberry32, seeded by the `seed` prop so the starting arrangement is reproducible) — 10 to 26 circles depending on panel area, each an SVG `<circle>` stroked in `--border`, `fill: none`. A single `setInterval` tick (380ms, no rAF, no canvas) runs pure physics over that array each cycle: every non-runoff droplet's radius grows by a small random amount and its position takes a small random jitter walk (both amounts roughly tripled for three ticks right after a merge, reading as a settle wobble); a single pairwise pass then merges any two droplets whose centers are closer than 0.85x their combined radii into one droplet positioned at their radius-weighted centroid with a radius equal to the square root of their summed squared radii (equal-area union); any droplet whose radius has crossed 7.6px stops growing and is promoted to a 'runoff' state with a randomized fall distance, meander offset and duration; and if the field has fallen under its target count, a replacement droplet is nucleated at a random position (avoiding the content block) with the same probability each tick. Every geometry change between ticks is smoothed by a 340ms CSS transition on cx/cy/r, so the field reads as continuous, faintly wobbling motion rather than 380ms snaps. A runoff droplet is handed to a small subcomponent that plays a single CSS keyframe animation: it drifts sideways and then straight down by CSS custom properties (`--dew-dx`, `--dew-dy`) over its own randomized duration, fading out as it goes, and calls back to remove itself from the field on `animationend` — a replacement then nucleates on a later tick. The instant a droplet is promoted, a matching rounded-pill `<rect>` (width from the droplet's own diameter, height equal to its fall distance, rotated a few degrees toward its meander direction) is added, capped at 6 concurrent tracks, as a hole inside the fog veil's SVG `<mask>` (`feGaussianBlur`-feathered, same technique as this registry's tour-spotlight): the pill grows from zero height to full over 900ms using the same mount-then-flip-state idiom as empty-state-pegboard's Slot (so a real CSS transition fires on first paint), holds for 2.6s once the fog there is genuinely gone, then fades its own hole back to nothing over 5.2s — the glass re-fogs, exactly like real condensation — and unmounts itself. A second hole in the same mask, sized from a live `ResizeObserver` measurement of the heading/body/CTA block plus 22px of padding, is always present and never fades: that patch is what keeps the copy and button legible, not merely their DOM stacking order above the veil. Colors are exclusively `--border` (droplets), `--ns-muted` (the fog fill) and the ordinary content tokens (`--foreground`, `--ns-muted`, `--ns-accent` on the CTA) — no hex, no canvas. Accessibility: the entire condensation SVG is `aria-hidden` and `pointer-events: none`; the heading, description and CTA are plain DOM stacked after it in source order at full token contrast, so legibility never depends on the animation; the CTA is an ordinary focusable `<button>` with a visible focus-visible ring. `prefers-reduced-motion: reduce` seeds a single sparse, static field (about 9 droplets) with no growth tick, no merging and no runoff — the permanent content-area hole in the fog mask is still present from the same `ResizeObserver` measurement, so the wiped patch around the copy is there from first paint, exactly as in full motion, just frozen."
  },
  "type": "registry:ui"
}