{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cascade",
  "type": "registry:ui",
  "title": "Cascade",
  "description": "Digital code rain: monospace glyph columns fall with bright white-hot heads and exponentially-fading cyan-steel trails, glyphs scrambling as the light passes, layered by depth. Pale-phosphor default (classic green is a preset). Canvas 2D, seeded, offscreen-paused, reduced-motion-safe.",
  "dependencies": [],
  "registryDependencies": [
    "https://crucibleui.com/r/use-visibility-pause.json",
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/backgrounds/cascade/cascade.tsx",
      "type": "registry:ui",
      "target": "components/crucible/cascade.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\nexport interface CascadeProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Two tones `[head, trail]`. `head` is the bright leading glyph, `trail` is\n   * the color the exponential tail fades toward. Defaults to pale phosphor:\n   * near-white heads melting into a cool cyan-steel trail. For the classic\n   * green rain pass `[\"#d6ffe0\", \"#22c55e\"]`.\n   * @default [\"#eafffb\", \"#38c7d6\"]\n   */\n  colors?: [string, string];\n  /** Fall-speed multiplier. 1 = default, 2 = twice as fast. @default 1 */\n  speed?: number;\n  /** Overall glyph brightness / bloom, 0–2. @default 1 */\n  intensity?: number;\n  /** Glyph size in CSS px — also the column width. Clamped 8–48. @default 16 */\n  fontSize?: number;\n  /** devicePixelRatio ceiling (text-heavy, so kept modest). Clamped 1–2. @default 2 */\n  dpr?: number;\n  /** Freeze the rain at a static, legible frame. @default false */\n  paused?: boolean;\n}\n\nconst DEFAULT_COLORS: [string, string] = [\"#eafffb\", \"#38c7d6\"];\n\n/** Deterministic per-column PRNG so streams never desync SSR/CSR. */\nfunction mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\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\n/** Fixed seed base — the frame is identical on server-render intent and client. */\nconst SEED = 0x9e3779b9;\n\n/** Clean techy glyph set: half-width katakana + digits + symbols + a few caps. */\nconst GLYPHS: string[] = (() => {\n  const chars: string[] = [];\n  for (let c = 0xff66; c <= 0xff9d; c++) chars.push(String.fromCharCode(c));\n  for (const s of \"0123456789\") chars.push(s);\n  for (const s of \"=+-*/<>|:.^~\") chars.push(s);\n  for (const s of \"ABCDEFHKMNPRSTXZ\") chars.push(s);\n  return chars;\n})();\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim();\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  const int = parseInt(h, 16);\n  if (Number.isNaN(int) || h.length !== 6) return [234, 255, 251];\n  return [(int >> 16) & 255, (int >> 8) & 255, int & 255];\n}\n\ninterface Column {\n  rng: () => number;\n  /** Depth 0..1 — nearer columns fall faster, brighter, longer. */\n  depth: number;\n  /** Cells per second (pre speed-multiplier). */\n  speed: number;\n  /** Trail length in cells. */\n  len: number;\n  /** Extra-luminous \"hero\" column. */\n  bright: boolean;\n  /** Head position in cell-rows (float, can be negative before entering). */\n  head: number;\n  /** Frozen head position for the reduced-motion / paused frame. */\n  staticHead: number;\n  /** One glyph index per screen row; mutates as the light passes. */\n  glyphs: Int16Array;\n}\n\ninterface Internals {\n  ctx: CanvasRenderingContext2D;\n  cell: number;\n  cols: number;\n  rows: number;\n  xOff: number;\n  width: number;\n  height: number;\n  columns: Column[];\n  head: [number, number, number];\n  trail: [number, number, number];\n  speed: number;\n  intensity: number;\n  fontSize: number;\n  bg: string;\n}\n\n/** Build the columns for a given grid, deterministically from the seed. */\nfunction buildColumns(cols: number, rows: number): Column[] {\n  const out: Column[] = [];\n  for (let i = 0; i < cols; i++) {\n    const rng = mulberry32(SEED + Math.imul(i + 1, 0x27d4eb2f));\n    const depth = rng();\n    const span = rows + 4;\n    const glyphs = new Int16Array(Math.max(rows, 1));\n    for (let r = 0; r < glyphs.length; r++) glyphs[r] = (rng() * GLYPHS.length) | 0;\n    out.push({\n      rng,\n      depth,\n      speed: 4 + depth * 13, // nearer = faster\n      len: Math.round(8 + rng() * 22 + depth * 6),\n      bright: rng() < 0.13,\n      head: -rng() * span, // staggered vertical start for depth\n      staticHead: rng() * span, // legible frozen frame\n      glyphs,\n    });\n  }\n  return out;\n}\n\nfunction respawn(c: Column, rows: number) {\n  c.depth = c.rng();\n  c.speed = 4 + c.depth * 13;\n  c.len = Math.round(8 + c.rng() * 22 + c.depth * 6);\n  c.bright = c.rng() < 0.13;\n  c.head = -c.rng() * 6 - 1; // re-enter just above the top\n  // Freshly scramble a few glyphs so the returning stream isn't a repeat.\n  for (let k = 0; k < 6; k++) {\n    const r = (c.rng() * rows) | 0;\n    if (r >= 0 && r < c.glyphs.length) c.glyphs[r] = (c.rng() * GLYPHS.length) | 0;\n  }\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n  return a + (b - a) * t;\n}\n\n/**\n * Render one frame. `useStatic` freezes every column at its `staticHead` and\n * skips glyph mutation — the reduced-motion / paused frame.\n */\nfunction draw(it: Internals, dt: number, useStatic: boolean) {\n  const { ctx, cell, rows, xOff, columns, head, trail } = it;\n\n  // Opaque near-black repaint (cool cast so the frame never reads dead #000).\n  ctx.globalCompositeOperation = \"source-over\";\n  ctx.fillStyle = it.bg;\n  ctx.fillRect(0, 0, it.width, it.height);\n\n  ctx.textAlign = \"center\";\n  ctx.textBaseline = \"middle\";\n  ctx.font = `${it.fontSize}px ui-monospace, \"SFMono-Regular\", \"Cascadia Code\", Menlo, Consolas, monospace`;\n\n  const gAmt = Math.max(it.intensity, 0);\n\n  // ------------------------------------------------------------- trail pass --\n  // Every glyph except the glowing head, drawn cheaply with source-over.\n  for (let i = 0; i < columns.length; i++) {\n    const c = columns[i];\n    const cx = xOff + i * cell + cell * 0.5;\n\n    if (!useStatic) {\n      c.head += c.speed * it.speed * dt;\n      if (c.head - c.len > rows + 1) respawn(c, rows);\n      // Head scramble + occasional trail mutation, deterministic via rng.\n      const hr = Math.floor(c.head);\n      if (hr >= 0 && hr < c.glyphs.length && c.rng() < 0.4) {\n        c.glyphs[hr] = (c.rng() * GLYPHS.length) | 0;\n      }\n      if (c.rng() < 0.09) {\n        const r = (c.rng() * c.glyphs.length) | 0;\n        c.glyphs[r] = (c.rng() * GLYPHS.length) | 0;\n      }\n    }\n\n    const headPos = useStatic ? c.staticHead : c.head;\n    const trailK = 4 / Math.max(c.len, 1);\n    const colAlpha = (0.5 + c.depth * 0.5) * (c.bright ? 1 : 0.82);\n\n    const r0 = Math.round(headPos);\n    const rTop = r0 - c.len;\n    for (let r = r0 + 1; r >= rTop; r--) {\n      if (r < 0 || r >= c.glyphs.length) continue;\n      if (r === r0) continue; // the head is drawn in the glow pass\n      const d = headPos - r;\n      // Long soft tail above the head, sharp cutoff below it.\n      const a = (d >= 0 ? Math.exp(-d * trailK) : Math.exp(d * 3.0)) * colAlpha * gAmt;\n      if (a < 0.015) continue;\n      const t = Math.min(Math.max(d / c.len, 0), 1);\n      const rr = lerp(head[0], trail[0], t);\n      const gg = lerp(head[1], trail[1], t);\n      const bb = lerp(head[2], trail[2], t);\n      ctx.fillStyle = `rgba(${rr | 0}, ${gg | 0}, ${bb | 0}, ${Math.min(a, 1)})`;\n      ctx.fillText(GLYPHS[c.glyphs[r]], cx, r * cell + cell * 0.5);\n    }\n  }\n\n  // -------------------------------------------------------------- head pass --\n  // Additive glow on the brightest glyph only — tasteful bloom, no wash.\n  ctx.globalCompositeOperation = \"lighter\";\n  for (let i = 0; i < columns.length; i++) {\n    const c = columns[i];\n    const headPos = useStatic ? c.staticHead : c.head;\n    const r0 = Math.round(headPos);\n    if (r0 < 0 || r0 >= c.glyphs.length) continue;\n    const cx = xOff + i * cell + cell * 0.5;\n    const cy = r0 * cell + cell * 0.5;\n\n    // Head tone pushed toward white; hero columns go fully white-hot.\n    const whiten = c.bright ? 0.85 : 0.55;\n    const hr = lerp(head[0], 255, whiten);\n    const hg = lerp(head[1], 255, whiten);\n    const hb = lerp(head[2], 255, whiten);\n    const glow = (c.bright ? 1 : 0.7) * gAmt;\n\n    ctx.shadowColor = `rgba(${head[0] | 0}, ${head[1] | 0}, ${head[2] | 0}, ${Math.min(glow, 1)})`;\n    ctx.shadowBlur = it.fontSize * (c.bright ? 0.9 : 0.6);\n    ctx.fillStyle = `rgba(${hr | 0}, ${hg | 0}, ${hb | 0}, ${Math.min(0.9 * gAmt, 1)})`;\n    ctx.fillText(GLYPHS[c.glyphs[r0]], cx, cy);\n  }\n  ctx.shadowBlur = 0;\n  ctx.globalCompositeOperation = \"source-over\";\n}\n\n/** Very-dark cool-tinted base derived from the trail hue — never flat #000. */\nfunction baseFill(trail: [number, number, number]): string {\n  return `rgb(${(3 + trail[0] * 0.012) | 0}, ${(5 + trail[1] * 0.018) | 0}, ${(7 + trail[2] * 0.02) | 0})`;\n}\n\n/** SVG film-grain, embedded — kills banding on the glow halos. Self-contained. */\nconst GRAIN =\n  \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\\\")\";\n\n/**\n * Cascade — digital code rain, done tastefully. Columns of monospace glyphs\n * fall down the frame: each a vertical stream with a bright, white-hot leading\n * head and an exponentially-fading tail, glyphs scrambling as the light passes\n * through them. Per-column speed, length and depth vary so the field reads\n * layered; occasional hero columns burn brighter. Defaults to a pale-phosphor\n * palette (near-white heads → cool cyan-steel trail on cool near-black), not\n * the cliché green — pass `colors` for the classic green rain. Canvas 2D:\n * seeded and SSR-safe, offscreen/hidden-tab paused, reduced-motion-safe (a\n * frozen, legible frame). Drop it inside any `relative` container.\n */\nexport function Cascade({\n  colors = DEFAULT_COLORS,\n  speed = 1,\n  intensity = 1,\n  fontSize = 16,\n  dpr = 2,\n  paused = false,\n  className,\n  ...props\n}: CascadeProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const internalsRef = React.useRef<Internals | null>(null);\n  const reducedMotion = useReducedMotion();\n  const staticFrame = paused || reducedMotion;\n  const staticFrameRef = React.useRef(staticFrame);\n  staticFrameRef.current = staticFrame;\n  const lastElapsedRef = React.useRef(0);\n\n  const fs = Math.min(Math.max(fontSize, 8), 48);\n\n  const tick = React.useCallback((elapsed: number) => {\n    const it = internalsRef.current;\n    if (!it) return;\n    let dt = elapsed - lastElapsedRef.current;\n    lastElapsedRef.current = elapsed;\n    // Clamp long gaps (tab restore) so streams don't teleport.\n    if (dt > 0.1) dt = 0.1;\n    if (dt < 0) dt = 0;\n    draw(it, dt, staticFrameRef.current);\n  }, []);\n\n  const containerRef = useVisibilityPause<HTMLDivElement>(tick, { paused: staticFrame });\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext(\"2d\", { alpha: false });\n    if (!ctx) return;\n\n    const effectiveDpr = Math.min(\n      typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1,\n      Math.min(Math.max(dpr, 1), 2)\n    );\n\n    const trail = hexToRgb(colors[1]);\n    const it: Internals = {\n      ctx,\n      cell: fs,\n      cols: 0,\n      rows: 0,\n      xOff: 0,\n      width: 0,\n      height: 0,\n      columns: [],\n      head: hexToRgb(colors[0]),\n      trail,\n      speed,\n      intensity,\n      fontSize: fs,\n      bg: baseFill(trail),\n    };\n    internalsRef.current = it;\n\n    // 2D context-loss: preventDefault asks the browser to restore, and the\n    // ResizeObserver repaint below refills the canvas once it comes back.\n    const onContextLost = (e: Event) => {\n      e.preventDefault();\n    };\n    canvas.addEventListener(\"contextlost\", onContextLost);\n\n    const resize = () => {\n      const w = Math.max(container.clientWidth, 1);\n      const h = Math.max(container.clientHeight, 1);\n      canvas.width = Math.round(w * effectiveDpr);\n      canvas.height = Math.round(h * effectiveDpr);\n      canvas.style.width = \"100%\";\n      canvas.style.height = \"100%\";\n      ctx.setTransform(effectiveDpr, 0, 0, effectiveDpr, 0, 0);\n\n      it.width = w;\n      it.height = h;\n      it.cols = Math.max(1, Math.floor(w / fs));\n      it.rows = Math.max(1, Math.ceil(h / fs) + 1);\n      it.xOff = (w - it.cols * fs) / 2;\n      it.columns = buildColumns(it.cols, it.rows);\n\n      // Paint one frame immediately — never a blank canvas on mount/resize.\n      draw(it, 0, staticFrameRef.current);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    return () => {\n      ro.disconnect();\n      canvas.removeEventListener(\"contextlost\", onContextLost);\n      internalsRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [dpr, fs, colors.join(\",\")]);\n\n  // Live-sync speed / intensity without rebuilding (and resetting) the rain.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it) return;\n    it.speed = speed;\n    it.intensity = intensity;\n    if (staticFrameRef.current) draw(it, 0, true);\n  }, [speed, intensity]);\n\n  // Re-render the frozen frame in place when paused/reduced-motion toggles.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it || !staticFrame) return;\n    draw(it, 0, true);\n  }, [staticFrame]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden\n      data-crucible=\"cascade\"\n      className={cn(\"absolute inset-0 -z-10 overflow-hidden bg-neutral-950\", className)}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n      {/* Soft vignette so the rain sinks into the frame edges. */}\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          background:\n            \"radial-gradient(120% 120% at 50% 42%, transparent 55%, rgba(0,0,0,0.55) 100%)\",\n        }}\n      />\n      {/* Embedded film-grain — dithers the glow halos, defeats 8-bit banding. */}\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{ backgroundImage: GRAIN, opacity: 0.04, mixBlendMode: \"overlay\" }}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}