{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"matrix-rain","type":"registry:ui","title":"MatrixRain","description":"Canvas-based falling glyph rain with configurable color, speed, and density","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["backgrounds"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/matrix-rain/MatrixRain.tsx","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { observeInView } from \"../../internals/motion/in-view\";\n\n/**\n * MatrixRain - Canvas2D \"digital rain\" background\n *\n * Columns of falling glyphs with a bright leading character and a dimmer\n * trailing one. The trail comes from repeatedly painting a low-opacity black\n * rectangle over the previous frame instead of clearing it, so `fadeOpacity`\n * controls trail length. The canvas fills its parent (`h-full w-full`) and\n * paints its own black background, so size it via a wrapping element.\n *\n * The frame loop pauses while the canvas is scrolled out of view and resumes\n * from the column state it left off at.\n */\nexport interface MatrixRainProps {\n\t/** Glyph color */\n\tcolor?: string;\n\t/** Fall speed multiplier (`< 1` slower, `> 1` faster) */\n\tspeed?: number;\n\t/** Column density multiplier (higher = more, narrower columns) */\n\tdensity?: number;\n\t/** Font size of each glyph in pixels */\n\tglyphSize?: number;\n\t/** Opacity of the black overlay painted each frame — controls trail length */\n\tfadeOpacity?: number;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Seed for the glyph and column-position stream. The same seed always\n\t * produces the same rain, which is what makes the effect reproducible in a\n\t * test and identical across two mounts of the same instance. Change it to\n\t * give two rains on one page different streams — the default is shared, so\n\t * two `<MatrixRain />` with no seed start out alike.\n\t */\n\tseed?: number;\n}\n\nconst GLYPHS =\n\t\"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&\";\n\n/**\n * mulberry32 — a tiny deterministic PRNG, standing in for the source's bare\n * `Math.random()`. Seeding costs nothing visually (the same uniform\n * distribution) and buys a rain that can be asserted on frame by frame, which\n * an unseeded canvas loop cannot be.\n */\nfunction mulberry32(seed: number): () => number {\n\tlet state = seed >>> 0;\n\treturn () => {\n\t\tstate = (state + 0x6d2b79f5) >>> 0;\n\t\tlet t = state;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\nexport function MatrixRain({\n\tcolor = \"#00ff41\",\n\tspeed = 1.0,\n\tdensity = 1.0,\n\tglyphSize = 16,\n\tfadeOpacity = 0.05,\n\tclassName,\n\tseed = 1,\n}: MatrixRainProps) {\n\tconst [canvas, canvasRef] = useElementRef<HTMLCanvasElement>();\n\n\t// Read inside the running loop only. The Svelte `$effect` reads these three\n\t// exclusively from `draw()`, which runs from a rAF callback and is therefore\n\t// outside its tracking context — so they must NOT tear the loop down and\n\t// restart it when they change. `glyphSize` and `density` are read\n\t// synchronously by `init()` in the effect body, which is why they ARE effect\n\t// dependencies below: changing either genuinely re-columns the rain.\n\tconst colorRef = useLiveRef(color);\n\tconst speedRef = useLiveRef(speed);\n\tconst fadeOpacityRef = useLiveRef(fadeOpacity);\n\n\t// Component-scoped in the source (`let frameCount = 0`), so it survives a\n\t// re-init rather than restarting the `speed < 1` frame-skip phase.\n\tconst frameCountRef = useRef(0);\n\n\tuseEffect(() => {\n\t\tif (!canvas) return;\n\n\t\tconst context = canvas.getContext(\"2d\");\n\t\tif (!context) return;\n\t\t// Re-bound with an explicit non-null type: `draw()` is a hoisted function\n\t\t// declaration, and TypeScript does not carry the null check above into one.\n\t\tconst ctx: CanvasRenderingContext2D = context;\n\n\t\tconst random = mulberry32(seed);\n\n\t\tconst safeGlyphSize = Math.max(1, glyphSize);\n\t\tconst safeDensity = Math.max(0.1, density);\n\t\t// Horizontal distance between two columns. `density` is documented as\n\t\t// \"higher = more, narrower columns\", so it DIVIDES the glyph width here:\n\t\t// doubling it halves the pitch, which both doubles the column count and\n\t\t// packs the streams tighter. The Svelte source multiplies instead, which\n\t\t// makes a higher density produce fewer, wider columns — the opposite of\n\t\t// what its own README describes. Deliberate divergence: the contract wins.\n\t\tconst columnPitch = safeGlyphSize / safeDensity;\n\n\t\tlet columns: number[] = [];\n\t\tlet canvasW = 0;\n\t\tlet canvasH = 0;\n\t\tlet rafId = 0;\n\n\t\t// `charAt` rather than `GLYPHS[i]`: index-identical for every character\n\t\t// in the set, and typed `string` under `noUncheckedIndexedAccess`.\n\t\tfunction randomGlyph(): string {\n\t\t\treturn GLYPHS.charAt(Math.floor(random() * GLYPHS.length));\n\t\t}\n\n\t\tfunction init(w: number, h: number) {\n\t\t\tconst colCount = Math.max(1, Math.floor(w / columnPitch));\n\t\t\tcolumns = Array.from({ length: colCount }, () => Math.floor(random() * (h / safeGlyphSize)));\n\t\t}\n\n\t\tfunction draw() {\n\t\t\tconst w = canvasW;\n\t\t\tconst h = canvasH;\n\n\t\t\tconst currentColor = colorRef.current;\n\t\t\tconst currentSpeed = speedRef.current;\n\t\t\tconst currentFadeOpacity = fadeOpacityRef.current;\n\n\t\t\t// Fade trail\n\t\t\tctx.fillStyle = `rgba(0, 0, 0, ${currentFadeOpacity})`;\n\t\t\tctx.fillRect(0, 0, w, h);\n\n\t\t\tctx.font = `${glyphSize}px monospace`;\n\t\t\tctx.shadowBlur = 8;\n\t\t\tctx.shadowColor = currentColor;\n\n\t\t\tconst rowsPerFrame = Math.max(1, currentSpeed);\n\t\t\tframeCountRef.current++;\n\n\t\t\t// Only advance every N frames to control speed below 1x\n\t\t\tconst shouldAdvance =\n\t\t\t\tcurrentSpeed >= 1 || frameCountRef.current % Math.round(1 / currentSpeed) === 0;\n\n\t\t\tif (shouldAdvance) {\n\t\t\t\tfor (let i = 0; i < columns.length; i++) {\n\t\t\t\t\tconst row = columns[i] as number;\n\t\t\t\t\tconst x = i * columnPitch;\n\t\t\t\t\tconst y = row * glyphSize;\n\n\t\t\t\t\t// Head character (bright white)\n\t\t\t\t\tctx.fillStyle = \"#ffffff\";\n\t\t\t\t\tctx.fillText(randomGlyph(), x, y);\n\n\t\t\t\t\t// Body glyph one step behind (dimmer)\n\t\t\t\t\tif (row > 1) {\n\t\t\t\t\t\tctx.fillStyle = currentColor;\n\t\t\t\t\t\tctx.fillText(randomGlyph(), x, y - glyphSize);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Reset column when it exits bottom, with random delay\n\t\t\t\t\tif (y > h && random() > 0.975) {\n\t\t\t\t\t\tcolumns[i] = 0;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolumns[i] = row + rowsPerFrame;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tctx.shadowBlur = 0;\n\t\t\trafId = requestAnimationFrame(draw);\n\t\t}\n\n\t\t// A queued frame is the loop's only \"is it running\" state, so `0` is the\n\t\t// idle sentinel — a real frame id is never 0. `start()` is therefore\n\t\t// safe to call again while the loop is already running.\n\t\tfunction start() {\n\t\t\tif (rafId !== 0) return;\n\t\t\trafId = requestAnimationFrame(draw);\n\t\t}\n\n\t\tfunction stop() {\n\t\t\tif (rafId === 0) return;\n\t\t\tcancelAnimationFrame(rafId);\n\t\t\trafId = 0;\n\t\t}\n\n\t\t// Set canvas size to container size with HiDPI scaling\n\t\tconst resize = () => {\n\t\t\tconst dpr = window.devicePixelRatio || 1;\n\t\t\tcanvasW = canvas.clientWidth;\n\t\t\tcanvasH = canvas.clientHeight;\n\t\t\tcanvas.width = Math.floor(canvasW * dpr);\n\t\t\tcanvas.height = Math.floor(canvasH * dpr);\n\t\t\tctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\t\t\tctx.fillStyle = \"black\";\n\t\t\tctx.fillRect(0, 0, canvasW, canvasH);\n\t\t\tinit(canvasW, canvasH);\n\t\t};\n\n\t\tresize();\n\n\t\tconst observer = new ResizeObserver(resize);\n\t\tobserver.observe(canvas);\n\n\t\tstart();\n\n\t\t// A rain used as a page background is off screen most of the time, and\n\t\t// every frame it paints there costs a full-canvas `fillRect` plus two\n\t\t// shadow-blurred `fillText` per column for nothing. Pause the loop while\n\t\t// the canvas is out of view and resume it on the way back: the trail is\n\t\t// an accumulation of fades over whatever is already on the canvas, so\n\t\t// resuming from the current column state needs no re-init and shows no\n\t\t// seam. `threshold: 0` — any sliver on screen counts as visible — and\n\t\t// `once: false`, because this has to keep reporting both ways.\n\t\t//\n\t\t// The loop still starts immediately rather than waiting for the first\n\t\t// observer callback: that callback lands a task later, and a background\n\t\t// that is already on screen should not hold a blank canvas until then.\n\t\tconst inViewHandle = observeInView(canvas, {\n\t\t\tonce: false,\n\t\t\tthreshold: 0,\n\t\t\tonChange: (intersecting) => {\n\t\t\t\tif (intersecting) start();\n\t\t\t\telse stop();\n\t\t\t},\n\t\t});\n\n\t\treturn () => {\n\t\t\tstop();\n\t\t\tinViewHandle.destroy();\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [canvas, glyphSize, density, seed, colorRef, speedRef, fadeOpacityRef]);\n\n\treturn <canvas ref={canvasRef} className={cn(\"block h-full w-full bg-black\", className)} />;\n}\n","type":"registry:ui","target":"@ui/fancy/components/matrix-rain/MatrixRain.tsx"},{"path":"react/src/components/matrix-rain/index.ts","content":"\"use client\";\n\nexport { MatrixRain } from \"./MatrixRain\";\nexport type { MatrixRainProps } from \"./MatrixRain\";\n","type":"registry:ui","target":"@ui/fancy/components/matrix-rain/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/matrix-rain?framework=react"}}