{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "brinicle-descent",
  "title": "Brinicle Descent",
  "description": "A full-bleed underwater backdrop modelled on brinicles: dense, sub-zero brine sinking off newly formed sea ice freezes a hollow ice sheath around itself as it descends, flashes an expanding freeze halo where it touches bottom, then breaks free and drifts off while a fresh tip nucleates elsewhere. Several overlapping lifecycles running at once so the pane is never empty.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/brinicle-descent/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\n\n// ---------------------------------------------------------------------------\n// BrinicleDescent — a full-bleed underwater backdrop grown from a real polar\n// mechanic: brine rejected by newly forming sea ice sinks as a dense, cold\n// plume, and because that plume sits at or below the surrounding water's\n// freezing point a thin ice sheath nucleates and grows AROUND it, extending\n// the plume downward as a hollow tube (a \"brinicle\" / ice stalactite —\n// documented in Antarctic/Arctic time-lapse footage and brine-drainage-channel\n// literature, e.g. Dayton & Martin 1971). Field/time-lapse descent rates\n// cluster 0.3-1.5 m/hr; sheath radius grows diffusion-limited (Stefan-problem\n// sqrt(t)) — thickest near the ceiling where a point has existed longest,\n// thinnest at the active tip. On reaching the seafloor the plume spreads into\n// an expanding freeze halo. A single downwelling site is transient: a tube\n// thickens, eventually detaches from the ice above and drifts off, while new\n// sites keep nucleating fresh tips elsewhere — never a single one-shot event.\n//\n// COMPRESSION: rendering ~2.5m of real water column, real descent at\n// 0.3-1.5 m/hr would take 1.7-8.3 hours to cross this frame. That is\n// compressed into an 8s on-screen descent (~1900-2900x real time) —\n// disclosed here rather than hidden, since this is an illustrative ambient\n// process, not a literal countdown.\n//\n// LIFECYCLE (per tube, a small state machine, several running staggered and\n// overlapping so the pane is never empty): descend (8s, tip travels top to\n// bottom) -> touchdown (a freeze halo expands/holds/fades at the seafloor,\n// 4.4s total, while the sheath keeps thickening) -> detach (the ceiling\n// attachment opens a gap, then the whole tube drifts down and fades, 1.9s) ->\n// removed, while a NEW tip nucleates elsewhere 3-6s after the prior tube\n// entered touchdown, so lifecycles overlap and the scene never resets empty.\n// The pane is pre-seeded at mount with tubes already mid-lifecycle (negative\n// phase-start offsets) rather than starting from a blank column, so t0 is\n// already alive.\n//\n// SHEATH RADIUS: for a point at height y on a tube, depositElapsed is when\n// the descending tip passed that y (linear in y along the 8s descent); its\n// age is (current time since descent start) - depositElapsed, and radius is\n// minR + (maxR-minR)*sqrt(clamp(age/8000, 0, 1)) — the ceiling end (oldest)\n// saturates toward maxR while the tip stays thin, and the whole tube keeps\n// visibly thickening for a while even after the tip stops moving.\n//\n// TOKENS AND THE THEME INVERSION: colors come only from --background,\n// --foreground, --ns-muted, --border via getComputedStyle(documentElement),\n// re-read on a MutationObserver watching its class. No manual \"if dark\n// theme\" branch anywhere: the water wash is mix(background, muted, 0.3) and\n// the tube core is mix(muted, foreground, 0.6) — because --foreground itself\n// already flips (near-white ink in dark theme, near-black ink in light\n// theme), mixing toward it automatically makes the tube read BRIGHT against\n// dark water and DARK against pale water, the correct per-theme inversion,\n// for free (the same trick dye-whorl uses, applied here without any explicit\n// theme check). The tube's rim (its strongest edge) is always drawn in pure\n// --foreground — the extreme ink value in whichever direction the theme\n// points. --ns-accent never appears; the touchdown halo's climactic flash is\n// pure luminance from --foreground.\n//\n// HOST: DPR-aware canvas sized off the host's own bounding box (never\n// window.resize), capped at 2. ResizeObserver re-sizes; IntersectionObserver\n// (threshold 0) and visibilitychange stop the single rAF loop when offscreen\n// or hidden; MutationObserver re-reads tokens and, when the loop itself is\n// stopped (reduced motion), repaints the frozen frame with the new colors.\n// prefers-reduced-motion composes and draws exactly one still frame: one\n// tube ~60% down its descent (thickened sheath, active tip) plus a second,\n// younger tip freshly nucleated near the top — the most structurally\n// complete single frame, never the near-empty literal t0.\n// ---------------------------------------------------------------------------\n\ntype Phase = \"descend\" | \"touchdown\" | \"detach\";\n\ninterface Tube {\n  id: number;\n  x0: number; // nucleation x, 0..1 fraction of width\n  phase: Phase;\n  phaseStart: number; // engine clock ms when the CURRENT phase began\n  descendStart: number; // engine clock ms when descent began (fixed for the tube's life)\n  seedA: number; // wobble phase\n  seedB: number; // reserved jitter\n}\n\ntype RGB = [number, number, number];\n\nconst DESCEND_MS = 8000;\nconst HALO_EXPAND_MS = 1800;\nconst HALO_HOLD_MS = 600;\nconst HALO_FADE_MS = 2000;\nconst HALO_TOTAL_MS = HALO_EXPAND_MS + HALO_HOLD_MS + HALO_FADE_MS;\nconst DETACH_GAP_MS = 400;\nconst DETACH_DRIFT_MS = 1500;\nconst DETACH_TOTAL_MS = DETACH_GAP_MS + DETACH_DRIFT_MS;\nconst SPAWN_MIN_MS = 3000;\nconst SPAWN_MAX_MS = 6000;\nconst SAMPLES = 22;\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]},${Math.max(0, 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 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 makeTube(id: number, rand: () => number, phaseStart: number): Tube {\n  return {\n    id,\n    x0: lerp(0.12, 0.88, rand()),\n    phase: \"descend\",\n    phaseStart,\n    descendStart: phaseStart,\n    seedA: rand() * 1000,\n    seedB: rand() * 1000,\n  };\n}\n\nexport interface BrinicleDescentProps {\n  /** max concurrent brinicles, 2-4. @default 3 */\n  density?: number;\n  /** clock speed multiplier. @default 1 */\n  speed?: number;\n  /** freeze the simulation in place without unmounting it. @default false */\n  paused?: boolean;\n  children?: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n}\n\nexport function BrinicleDescent({\n  density = 3,\n  speed = 1,\n  paused = false,\n  children,\n  className = \"\",\n  style,\n}: BrinicleDescentProps) {\n  const reduced = useReducedMotion();\n  const hostRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  const densityRef = useRef(density);\n  densityRef.current = Math.round(Math.min(4, Math.max(2, density)));\n  const speedRef = useRef(speed);\n  speedRef.current = speed;\n  const pausedRef = useRef(paused);\n  pausedRef.current = paused;\n\n  const engineRef = useRef({\n    w: 0,\n    h: 0,\n    dpr: 1,\n    clock: 0,\n    nextSpawnAt: 4000,\n    nextId: 1,\n    tubes: [] as Tube[],\n    raf: 0,\n    lastNow: 0,\n    visible: true,\n    reduced: false,\n    background: [10, 10, 10] as RGB,\n    foreground: [237, 237, 237] as RGB,\n    muted: [141, 141, 141] as RGB,\n    rand: mulberry32(1),\n  });\n\n  useEffect(() => {\n    const host = hostRef.current;\n    const canvas = canvasRef.current;\n    if (!host || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const e = engineRef.current;\n\n    const syncColors = () => {\n      const root = document.documentElement;\n      e.background = readToken(root, \"--background\", e.background);\n      e.foreground = readToken(root, \"--foreground\", e.foreground);\n      e.muted = readToken(root, \"--ns-muted\", e.muted);\n    };\n\n    const resize = () => {\n      const r = host.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    };\n\n    // Draw one tube (sheath + rim) and, while in touchdown, its freeze halo.\n    const drawTube = (tube: Tube, clock: number) => {\n      const minDim = Math.min(e.w, e.h);\n      const minR = minDim / 180;\n      const maxR = minDim / 70;\n      const topY = e.h * 0.06;\n      const bottomY = e.h * 0.94;\n      const columnH = bottomY - topY;\n      const wobbleAmp = minDim * 0.018;\n      const wobbleFreq = 0.018;\n\n      const descendAge = clamp01((clock - tube.descendStart) / DESCEND_MS) * DESCEND_MS;\n      const tipY = topY + columnH * (descendAge / DESCEND_MS);\n\n      let topCut = topY;\n      let dyOffset = 0;\n      let globalAlpha = 1;\n\n      if (tube.phase === \"detach\") {\n        const age = clock - tube.phaseStart;\n        if (age <= DETACH_GAP_MS) {\n          const t = age / DETACH_GAP_MS;\n          topCut = topY + columnH * 0.05 * t;\n        } else {\n          topCut = topY + columnH * 0.05;\n          const t2 = clamp01((age - DETACH_GAP_MS) / DETACH_DRIFT_MS);\n          dyOffset = 40 * t2;\n          globalAlpha = 1 - t2;\n        }\n      }\n\n      if (globalAlpha <= 0.002) return;\n\n      const xAt = (y: number) => e.w * tube.x0 + wobbleAmp * Math.sin(y * wobbleFreq + tube.seedA);\n      const radiusAt = (y: number) => {\n        const depositElapsed = ((y - topY) / columnH) * DESCEND_MS;\n        const age = clamp01((clock - tube.descendStart - depositElapsed) / DESCEND_MS) * DESCEND_MS;\n        return minR + (maxR - minR) * Math.sqrt(age / DESCEND_MS);\n      };\n\n      const from = Math.min(topCut, tipY);\n      const to = Math.max(topCut, tipY);\n      if (to - from < 1) return;\n\n      const pts: { x: number; y: number; r: number }[] = [];\n      for (let i = 0; i <= SAMPLES; i++) {\n        const y = lerp(from, to, i / SAMPLES);\n        pts.push({ x: xAt(y), y: y + dyOffset, r: radiusAt(y) });\n      }\n\n      const core = mixRgb(e.muted, e.foreground, 0.6);\n      ctx.fillStyle = rgbaStr(core, 0.85 * globalAlpha);\n      ctx.beginPath();\n      for (let i = 0; i < pts.length; i++) {\n        const p = pts[i]!;\n        if (i === 0) ctx.moveTo(p.x - p.r, p.y);\n        else ctx.lineTo(p.x - p.r, p.y);\n      }\n      for (let i = pts.length - 1; i >= 0; i--) {\n        const p = pts[i]!;\n        ctx.lineTo(p.x + p.r, p.y);\n      }\n      ctx.closePath();\n      ctx.fill();\n\n      ctx.strokeStyle = rgbaStr(e.foreground, 0.45 * globalAlpha);\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      pts.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x - p.r, p.y) : ctx.lineTo(p.x - p.r, p.y)));\n      ctx.stroke();\n      ctx.beginPath();\n      pts.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x + p.r, p.y) : ctx.lineTo(p.x + p.r, p.y)));\n      ctx.stroke();\n\n      if (tube.phase === \"touchdown\") {\n        const age = clock - tube.phaseStart;\n        const tipX = xAt(bottomY);\n        let r = 2;\n        let a = 0;\n        if (age < HALO_EXPAND_MS) {\n          const t = age / HALO_EXPAND_MS;\n          r = lerp(2, minDim * 0.15, easeOutCubic(t));\n          a = 0.85 * t;\n        } else if (age < HALO_EXPAND_MS + HALO_HOLD_MS) {\n          r = minDim * 0.15;\n          a = 0.85;\n        } else {\n          r = minDim * 0.15;\n          const t = clamp01((age - HALO_EXPAND_MS - HALO_HOLD_MS) / HALO_FADE_MS);\n          a = 0.85 * (1 - t);\n        }\n        if (a > 0.002) {\n          const grad = ctx.createRadialGradient(tipX, bottomY, 0, tipX, bottomY, Math.max(1, r));\n          grad.addColorStop(0, rgbaStr(e.foreground, a));\n          grad.addColorStop(1, rgbaStr(e.foreground, 0));\n          ctx.fillStyle = grad;\n          ctx.beginPath();\n          ctx.arc(tipX, bottomY, Math.max(1, r), 0, Math.PI * 2);\n          ctx.fill();\n        }\n      }\n    };\n\n    const paint = (clock: number) => {\n      ctx.setTransform(e.dpr, 0, 0, e.dpr, 0, 0);\n      const water = mixRgb(e.background, e.muted, 0.3);\n      ctx.fillStyle = rgbaStr(water, 1);\n      ctx.fillRect(0, 0, e.w, e.h);\n      for (const tube of e.tubes) drawTube(tube, clock);\n    };\n\n    const advance = (dtMs: number) => {\n      e.clock += dtMs;\n      const maxTubes = densityRef.current;\n\n      for (const tube of e.tubes) {\n        const age = e.clock - tube.phaseStart;\n        if (tube.phase === \"descend\" && age >= DESCEND_MS) {\n          tube.phase = \"touchdown\";\n          tube.phaseStart = e.clock;\n        } else if (tube.phase === \"touchdown\" && age >= HALO_TOTAL_MS) {\n          tube.phase = \"detach\";\n          tube.phaseStart = e.clock;\n        }\n      }\n      e.tubes = e.tubes.filter((t) => !(t.phase === \"detach\" && e.clock - t.phaseStart >= DETACH_TOTAL_MS));\n\n      if (e.tubes.length === 0) {\n        e.tubes.push(makeTube(e.nextId++, e.rand, e.clock));\n        e.nextSpawnAt = e.clock + lerp(SPAWN_MIN_MS, SPAWN_MAX_MS, e.rand());\n      } else if (e.clock >= e.nextSpawnAt && e.tubes.length < maxTubes) {\n        e.tubes.push(makeTube(e.nextId++, e.rand, e.clock));\n        e.nextSpawnAt = e.clock + lerp(SPAWN_MIN_MS, SPAWN_MAX_MS, e.rand());\n      }\n    };\n\n    const drawStatic = () => {\n      // reduced motion: one composed still frame — one tube ~60% down its\n      // descent (thickened sheath, active tip) plus a younger tip just\n      // nucleated near the top. Built directly, never simulated forward.\n      const rand = mulberry32(9001);\n      const tubes: Tube[] = [\n        { id: 1, x0: 0.34, phase: \"descend\", phaseStart: -DESCEND_MS * 0.6, descendStart: -DESCEND_MS * 0.6, seedA: rand() * 1000, seedB: 0 },\n        { id: 2, x0: 0.68, phase: \"descend\", phaseStart: -300, descendStart: -300, seedA: rand() * 1000, seedB: 0 },\n      ];\n      const prevTubes = e.tubes;\n      e.tubes = tubes;\n      paint(0);\n      e.tubes = prevTubes;\n    };\n\n    resize();\n    syncColors();\n    e.reduced = reduced;\n\n    if (reduced) {\n      drawStatic();\n    } else {\n      // pre-seed with tubes already mid-lifecycle so t0 is already alive,\n      // never a blank column.\n      const rand = mulberry32(7);\n      e.rand = rand;\n      e.clock = 0;\n      e.tubes = [\n        makeTube(e.nextId++, rand, -DESCEND_MS * 0.7),\n        makeTube(e.nextId++, rand, -DESCEND_MS * 0.2),\n      ];\n      e.nextSpawnAt = lerp(SPAWN_MIN_MS, SPAWN_MAX_MS, rand());\n      paint(e.clock);\n    }\n\n    const loop = (now: number) => {\n      const dtMs = Math.min(48, now - e.lastNow);\n      e.lastNow = now;\n      if (!pausedRef.current) {\n        advance(dtMs * speedRef.current);\n      }\n      paint(e.clock);\n      e.raf = e.visible ? requestAnimationFrame(loop) : 0;\n    };\n\n    if (!reduced) {\n      e.lastNow = performance.now();\n      e.raf = requestAnimationFrame(loop);\n    }\n\n    const ro = new ResizeObserver(() => {\n      resize();\n      if (reduced) drawStatic();\n      else paint(e.clock);\n    });\n    ro.observe(host);\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        e.visible = !!entry?.isIntersecting;\n        if (e.visible && !reduced && !e.raf) {\n          e.lastNow = performance.now();\n          e.raf = requestAnimationFrame(loop);\n        } else if (!e.visible && e.raf) {\n          cancelAnimationFrame(e.raf);\n          e.raf = 0;\n        }\n      },\n      { threshold: 0 }\n    );\n    io.observe(host);\n\n    const onVisibility = () => {\n      if (document.hidden && e.raf) {\n        cancelAnimationFrame(e.raf);\n        e.raf = 0;\n      } else if (!document.hidden && e.visible && !reduced && !e.raf) {\n        e.lastNow = performance.now();\n        e.raf = requestAnimationFrame(loop);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    const mo = new MutationObserver(() => {\n      syncColors();\n      if (reduced || !e.raf) {\n        if (reduced) drawStatic();\n        else paint(e.clock);\n      }\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      document.removeEventListener(\"visibilitychange\", onVisibility);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [reduced]);\n\n  return (\n    <div ref={hostRef} className={`relative h-full w-full overflow-hidden ${className}`} style={style}>\n      <canvas ref={canvasRef} aria-hidden role=\"presentation\" className=\"pointer-events-none absolute inset-0 h-full w-full\" />\n      {children ? <div className=\"pointer-events-none absolute inset-0\">{children}</div> : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/brinicle-descent.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": "loud",
    "tags": [
      "background",
      "hero",
      "canvas",
      "simulation",
      "monochrome",
      "decorative",
      "ice"
    ],
    "instruction": "Build <BrinicleDescent density? speed? paused? children? className? style?> as a full-bleed 2D-canvas underwater pane growing brinicles — brine-rejection ice tubes, not a heightfield or shader trick. REAL MECHANIC: sea ice forming above a water column rejects brine; the plume is colder/denser than the water below and sinks, and because it sits at or below the freezing point a thin ice sheath nucleates and grows around it as it descends, extending a hollow tube downward (documented Antarctic/Arctic time-lapse footage; brine-drainage-channel literature, e.g. Dayton & Martin 1971). Field/time-lapse descent rates cluster 0.3-1.5 m/hr. LIFECYCLE PER TUBE, several running staggered and overlapping (2-4 concurrent, controlled by `density`, default 3) so the scene is never empty: descend (8000ms — tip travels linearly from top to bottom of a column representing ~2.5m of water, an explicit ~1900-2900x compression of the real 1.7-8.3hr crossing, documented not hidden) -> touchdown (4400ms total: a freeze halo at the seafloor expands 1800ms via easeOutCubic to radius = 0.15 * min(w,h), holds 600ms at full alpha 0.85, fades 2000ms to zero, while the sheath keeps thickening) -> detach (1900ms: first 400ms opens a small gap at the ceiling attachment by nudging the tube's top cutoff down 5% of the column height, then over 1500ms the whole tube drifts down 40px while its alpha eases 1 to 0) -> removed. A new tip nucleates via `makeTube` at a fresh random x whenever live tube count is below `density` and an engine-clock timer (jittered 3000-6000ms after the last spawn) elapses, or immediately if the tube list ever empties (a hard guarantee against a blank frame). The pane is PRE-SEEDED at mount with two tubes already mid-lifecycle (negative phase-start offsets relative to the engine clock, not simulated forward) so t0 is already alive rather than a blank column filling in. SHEATH RADIUS: for a sampled point at height y, depositElapsed = (y-topY)/columnHeight * 8000 is when the descending tip passed that y; that point's age is clamp(engineClock - tube.descendStart - depositElapsed, 0, 8000), and radius = minR + (maxR-minR)*sqrt(age/8000), with minR/maxR derived from the container's smaller dimension (/180 and /70) — the ceiling end (oldest) saturates toward maxR while the tip stays thin, and the whole tube keeps visibly thickening even once its tip has stopped moving. Each tube also carries a gentle sinusoidal x-wobble (amplitude ~1.8% of the smaller dimension, seeded per tube) so tubes read as organic descending plumes, not rigid lines. RENDERING: one 2D canvas, `w-full h-full`, geometry entirely derived from the host's own bounding box (never window size), redrawn every frame by filling a water wash then, per live tube, a filled quad-strip polygon (sampled at 22 points from cutoff to tip, offset left/right by radiusAt(y)) plus thin rim strokes down both edges, plus a radial-gradient touchdown halo when in that phase. TOKENS, NO MANUAL THEME BRANCH: colors read only from --background, --foreground, --ns-muted via getComputedStyle(document.documentElement), re-read on a MutationObserver watching its class, with no paint before that first read. Water wash = mix(background, muted, 0.3). Tube core = mix(muted, foreground, 0.6). Tube rim and the touchdown halo are pure foreground. Because --foreground itself already flips (near-white ink in dark theme, near-black ink in light theme), mixing toward it makes the tube automatically read bright against dark water in dark theme and dark against pale water in light theme — the correct per-theme value inversion with zero explicit theme detection, the same trick dye-whorl uses. --ns-accent never appears anywhere in the render. HOST: DPR-aware canvas capped at 2, sized off a ResizeObserver on the host div. A single rAF loop (engine-clock driven, dt capped at 48ms and scaled by `speed`) is stopped by an IntersectionObserver (threshold 0) when scrolled offscreen and by `visibilitychange` when the tab is hidden, and restarted cleanly on return. `paused` freezes the engine clock in place (loop keeps repainting the same instant) without unmounting anything. prefers-reduced-motion composes and draws exactly ONE still frame, built directly rather than simulated forward: one tube 60% down its descent (thickened sheath, active tip) plus a second, younger tip freshly nucleated near the top — the most structurally complete single frame, never the near-empty literal t0 — and the MutationObserver still repaints that frozen frame on a theme change. The canvas is aria-hidden/role=presentation; children render in an absolutely-positioned overlay above it and need a token scrim (bg-background/70 + backdrop-blur) since the water wash spans the full value range in both themes."
  },
  "type": "registry:ui"
}