{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-ascii-rainfall",
  "title": "Hero ASCII Rainfall",
  "description": "A full-bleed ASCII precipitation hero: every column runs its own independent falling drop with a fixed rows/sec speed and trail length, and the pointer bends nearby streams sideways like wind gusting through rain instead of painting or warping cells directly.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/hero-ascii-rainfall/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// ---------------------------------------------------------------------------\n// Downpour — a full-bleed ASCII precipitation hero. Unlike a stamped heat grid\n// or a sampled scalar field, this is a genuine per-column PARTICLE simulation:\n// every column owns one falling \"drop\" (a float head position in row-units, a\n// rows/sec fall speed and a trail length), advancing independently every\n// frame and wrapping back above the top once its trail clears the bottom —\n// the same treadmill trick a starfield uses, so the loop never resets\n// visibly. A column's glyph only gets reassigned the instant the head enters\n// a NEW row (not every frame), so a settled trail doesn't flicker; luminance\n// is purely a function of distance-from-head, gamma-shaped so the head reads\n// bright and the tail fades to ink. The pointer doesn't paint or warp\n// individual cells — it drives WIND: a per-column lateral pixel offset,\n// gaussian-weighted by horizontal distance to the pointer and gusting on a\n// per-column sine so nearby streams bend independently rather than as one\n// rigid sheet, with the gust's overall amplitude rising on pointer speed and\n// relaxing over ~1s once it stops, so the curtain settles back to falling\n// straight down on its own.\n// ---------------------------------------------------------------------------\n\nconst RAIN_CHARS = \"|:.'`,;\";\nconst SPEED_MIN = 9; // rows/s\nconst SPEED_MAX = 22;\nconst TRAIL_MIN = 6; // rows\nconst TRAIL_MAX = 18;\nconst WIND_RADIUS_COLS = 10; // gaussian falloff sigma, in columns\nconst WIND_MAX_PX = 14; // max lateral bend at full gust energy\nconst WIND_GUST_FREQ_MIN = 0.6; // rad/s\nconst WIND_GUST_FREQ_MAX = 1.4;\nconst WIND_EASE = 0.1; // per-frame lerp toward target offset\nconst ENERGY_GAIN = 0.05;\nconst ENERGY_TAU = 1.0; // s — relaxation time constant\nconst DT_MAX = 0.05;\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\nexport interface DownpourProps {\n  /** grid cell size in px */\n  cellSize?: number;\n  /** headline / CTA rendered over the field */\n  children?: ReactNode;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function Downpour({\n  cellSize = 13,\n  children,\n  className = \"\",\n}: DownpourProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    let fg = \"currentColor\";\n    let cellW = cellSize;\n    let cellH = cellSize;\n    let cols = 0;\n    let rows = 0;\n    let dpr = 1;\n    let sized = false;\n    let ready = false;\n    let disposed = false;\n\n    let headRow = new Float32Array(0);\n    let speed = new Float32Array(0);\n    let trailLen = new Float32Array(0);\n    let lastRow = new Int32Array(0);\n    let charBuf = new Uint8Array(0);\n    let windOffset = new Float32Array(0);\n    let windPhase = new Float32Array(0);\n    let windFreq = new Float32Array(0);\n\n    const readTokens = () => {\n      fg = getComputedStyle(canvas).color;\n    };\n\n    const measureCell = (fontFamily: string) => {\n      const off = document.createElement(\"canvas\");\n      const octx = off.getContext(\"2d\");\n      if (!octx) return;\n      octx.font = `${cellSize}px ${fontFamily}`;\n      cellW = Math.max(4, octx.measureText(\"MMMMMMMMMM\").width / 10);\n      cellH = cellSize;\n    };\n\n    const rand = mulberry32(0x5a17a1);\n\n    const seedColumn = (c: number, initial: boolean) => {\n      speed[c] = SPEED_MIN + rand() * (SPEED_MAX - SPEED_MIN);\n      trailLen[c] = TRAIL_MIN + rand() * (TRAIL_MAX - TRAIL_MIN);\n      // stagger restarts: begin above the top by a random multiple of the\n      // column's own trail so drops never fall back in visible unison\n      headRow[c] = initial\n        ? rand() * rows\n        : -(rand() * trailLen[c]! * 3);\n      lastRow[c] = -1e6;\n      windPhase[c] = rand() * Math.PI * 2;\n      windFreq[c] = WIND_GUST_FREQ_MIN + rand() * (WIND_GUST_FREQ_MAX - WIND_GUST_FREQ_MIN);\n    };\n\n    const resize = () => {\n      const { width, height } = canvas.getBoundingClientRect();\n      if (width < 2 || height < 2) {\n        sized = false;\n        return;\n      }\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      const fontFamily = getComputedStyle(canvas).fontFamily;\n      measureCell(fontFamily);\n      ctx.font = `${cellSize}px ${fontFamily}`;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n\n      // ceil, not floor — flooring leaves an unpainted strip along the\n      // bottom/right edge; the root clips, so overdraw is free.\n      cols = Math.max(4, Math.ceil(width / cellW));\n      rows = Math.max(4, Math.ceil(height / cellH));\n\n      headRow = new Float32Array(cols);\n      speed = new Float32Array(cols);\n      trailLen = new Float32Array(cols);\n      lastRow = new Int32Array(cols);\n      charBuf = new Uint8Array(cols * rows);\n      windOffset = new Float32Array(cols);\n      windPhase = new Float32Array(cols);\n      windFreq = new Float32Array(cols);\n\n      for (let c = 0; c < cols; c++) seedColumn(c, true);\n      sized = true;\n    };\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const onResize = () => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        resize();\n        if (reduced) drawStatic();\n      }, 150);\n    };\n\n    // advance the simulation by dt (no-op for the static reduced-motion frame)\n    const step = (dt: number) => {\n      for (let c = 0; c < cols; c++) {\n        headRow[c]! += speed[c]! * dt;\n        if (headRow[c]! - trailLen[c]! > rows) {\n          seedColumn(c, false);\n          continue;\n        }\n        const newRow = Math.floor(headRow[c]!);\n        if (newRow !== lastRow[c] && newRow >= 0 && newRow < rows) {\n          charBuf[newRow * cols + c] = Math.floor(rand() * RAIN_CHARS.length);\n          lastRow[c] = newRow;\n        }\n      }\n    };\n\n    const draw = (t: number) => {\n      if (!sized) return;\n      const w = cols * cellW;\n      const h = rows * cellH;\n      ctx.clearRect(0, 0, w, h);\n      ctx.fillStyle = fg;\n      for (let c = 0; c < cols; c++) {\n        const head = headRow[c]!;\n        const trail = trailLen[c]!;\n        const x = c * cellW + cellW / 2 + windOffset[c]!;\n        const rFrom = Math.max(0, Math.ceil(head - trail));\n        const rTo = Math.min(rows - 1, Math.floor(head));\n        for (let r = rFrom; r <= rTo; r++) {\n          const dist = head - r;\n          const lum = 1 - dist / trail;\n          if (lum <= 0.03) continue;\n          const alpha = Math.pow(lum, 1.5);\n          ctx.globalAlpha = 0.08 + alpha * 0.92;\n          const ch = RAIN_CHARS[charBuf[r * cols + c]!] ?? \".\";\n          ctx.fillText(ch, x, r * cellH + cellH / 2);\n        }\n      }\n      ctx.globalAlpha = 1;\n      void t;\n    };\n\n    // static reduced-motion frame: fill every column full-height so the\n    // silhouette of the effect is legible at rest, no animation, no listeners\n    const drawStatic = () => {\n      if (!sized) return;\n      for (let c = 0; c < cols; c++) {\n        headRow[c] = rows - 1;\n        trailLen[c] = rows;\n        for (let r = 0; r < rows; r++) {\n          charBuf[r * cols + c] = Math.floor(rand() * RAIN_CHARS.length);\n        }\n        windOffset[c] = 0;\n      }\n      draw(0);\n    };\n\n    // -- hot-path state -------------------------------------------------------\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const pointer = { x: -1e5, y: -1e5, tx: -1e5, ty: -1e5, energy: 0, has: false };\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(DT_MAX, (now - last) / 1000) : 1 / 60;\n      last = now;\n      t += dt;\n      step(dt);\n\n      pointer.energy *= Math.exp(-dt / ENERGY_TAU);\n      const pointerCol = pointer.has ? pointer.x / cellW : -1e5;\n      for (let c = 0; c < cols; c++) {\n        let target = 0;\n        if (pointer.has && pointer.energy > 0.004) {\n          const dCols = c - pointerCol;\n          const falloff = Math.exp(-(dCols * dCols) / (2 * WIND_RADIUS_COLS * WIND_RADIUS_COLS));\n          const gust = Math.sin(t * windFreq[c]! + windPhase[c]!);\n          target = falloff * pointer.energy * WIND_MAX_PX * gust;\n        }\n        windOffset[c]! += (target - windOffset[c]!) * WIND_EASE;\n      }\n\n      draw(t);\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = root.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n      if (pointer.has) {\n        const dx = x - pointer.tx;\n        const dy = y - pointer.ty;\n        const speedPx = Math.hypot(dx, dy);\n        pointer.energy = Math.min(1, pointer.energy + speedPx * ENERGY_GAIN * 0.02);\n      }\n      pointer.tx = x;\n      pointer.ty = y;\n      pointer.x = x;\n      pointer.y = y;\n      pointer.has = true;\n    };\n    const onPointerLeave = () => {\n      pointer.has = false;\n    };\n\n    const onVis = () => {\n      if (!document.hidden && !reduced && ready) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw(t);\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      readTokens();\n      resize();\n      ready = true;\n      if (reduced) {\n        drawStatic();\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    window.addEventListener(\"resize\", onResize);\n    if (!reduced) {\n      root.addEventListener(\"pointermove\", onPointerMove);\n      root.addEventListener(\"pointerleave\", onPointerLeave);\n    }\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      mo.disconnect();\n      window.removeEventListener(\"resize\", onResize);\n      root.removeEventListener(\"pointermove\", onPointerMove);\n      root.removeEventListener(\"pointerleave\", onPointerLeave);\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [cellSize]);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative isolate w-full overflow-hidden bg-background font-mono ${\n        /\\bmin-h-/.test(className) ? \"\" : \"min-h-screen\"\n      } ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"absolute inset-0 block h-full w-full text-foreground\"\n      />\n      {children ? (\n        <div className=\"relative z-10 flex h-full w-full flex-col items-start justify-end gap-4 p-8 sm:p-14\">\n          {children}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/hero-ascii-rainfall.tsx"
    }
  ],
  "meta": {
    "collection": "loud",
    "tags": [
      "ascii",
      "hero",
      "background",
      "canvas",
      "cursor",
      "rain",
      "particles"
    ],
    "instruction": "Build a full-bleed Canvas 2D hero: one independent falling drop per grid column, each a float head-row position advancing at its own fixed rows/sec speed (9-22) with its own trail length (6-18 rows), seeded via a deterministic mulberry32 PRNG (never Math.random) so a session's rainfall is reproducible. A column's glyph buffer only gets a fresh random character (from '|:.\\'\\`,;') the instant the head crosses into a NEW row — never every frame — so a settled trail holds still instead of flickering; luminance is purely distance-from-head, gamma-shaped (pow 1.5) so the head reads bright and the tail fades toward the ink floor, alpha never fully to zero (0.08 floor) so the tail dissolves rather than hard-cutting. When a column's head clears rows + its trail length past the bottom, it restarts above the top at a randomized negative offset (a random multiple of its own new trail length) with freshly randomized speed and trail — this staggering is what keeps drops from ever resetting in visible unison. WIND, not paint: the pointer maintains a rising/decaying 0..1 'energy' scalar (gains on pointer speed, relaxes over ~1s once it stops moving, exactly the plasma/wake decay pattern but applied to a lateral offset instead of a stamp or sample warp) and every column computes a target horizontal pixel offset as a gaussian falloff of its distance (in columns, sigma 10) from the pointer's column, times that energy, times a per-column sine gust (each column has its own frequency 0.6-1.4 rad/s and phase so nearby streams don't bend in lockstep) capped at 14px — the whole column's rendered x is offset uniformly by this eased value, so streams bend like a curtain in a gust and glide back straight once the pointer stops and energy decays to zero. Direct-DOM rAF, zero React state on the hot path; all typed arrays sized once per resize (cols/rows computed with Math.ceil, never floor, since flooring leaves an unpainted strip along the clipped container's bottom/right edge). Ink is read once via getComputedStyle(canvas).color and re-derived on a documentElement class MutationObserver for live theme flips. Mono cell dimensions are measured via an offscreen canvas's measureText after document.fonts.ready. prefers-reduced-motion fills every column full-height with one static randomized frame (no head/trail concept, no animation, no pointer listeners attached at all) so the effect's silhouette is legible without motion. Loop pauses on document.hidden and resumes on visibilitychange. Optional children render over the field, bottom-left anchored with padding. Props: cellSize (grid cell px, default 13), children, className.",
    "rank": 11
  },
  "type": "registry:ui"
}