{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"flickering-grid","type":"registry:ui","title":"FlickeringGrid","description":"Canvas grid of squares that flicker in and out of opacity at random each frame, pausing automatically via IntersectionObserver when scrolled off-screen and resizing to fit its container via ResizeObserver","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["backgrounds"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/flickering-grid/FlickeringGrid.tsx","content":"\"use client\";\n\nimport { useEffect } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\n\n/**\n * FlickeringGrid - Canvas-based grid with flickering opacity squares\n *\n * Uses ResizeObserver, IntersectionObserver, and requestAnimationFrame\n * for performant rendering with automatic pause when off-screen.\n */\nexport interface FlickeringGridProps {\n\t/** Size of each grid square in pixels */\n\tsquareSize?: number;\n\t/** Gap between squares in pixels */\n\tgridGap?: number;\n\t/** Probability of a square changing opacity each second (0-1) */\n\tflickerChance?: number;\n\t/** Color of the squares: a hex string, or `currentColor` (default) to follow the text colour */\n\tcolor?: string;\n\t/** Maximum opacity of squares (0-1) */\n\tmaxOpacity?: number;\n\t/** Fixed width in pixels (defaults to container width) */\n\twidth?: number;\n\t/** Fixed height in pixels (defaults to container height) */\n\theight?: number;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Seed for the flicker stream. The same seed always produces the same\n\t * sequence of opacities, 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 grids on one page different flicker — the default is shared, so\n\t * two `<FlickeringGrid />` with no seed light up alike.\n\t */\n\tseed?: number;\n}\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 grid 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\ninterface Rgb {\n\tr: number;\n\tg: number;\n\tb: number;\n}\n\nconst CURRENT_COLOR = /^\\s*currentcolor\\s*$/i;\n\nfunction parseHex(hex: string): Rgb {\n\tlet h = (hex || \"#000\").trim().replace(/^#/, \"\");\n\tif (h.length === 3) {\n\t\th = h\n\t\t\t.split(\"\")\n\t\t\t.map((c) => c + c)\n\t\t\t.join(\"\");\n\t}\n\tconst bigint = parseInt(h, 16) || 0;\n\treturn { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 };\n}\n\n/**\n * Turn the browser's computed `color` into 0-255 channels. The common\n * `rgb()`/`rgba()` serialisation is parsed directly; newer spaces (oklch,\n * color-mix, ...) are rasterised through a 1x1 canvas, which converts them to\n * sRGB for free. Falls back to black when neither works (no canvas backend).\n */\nfunction computedToRgb(computed: string): Rgb {\n\tconst m = /^rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/i.exec(computed);\n\tif (m) return { r: Math.round(+m[1]!), g: Math.round(+m[2]!), b: Math.round(+m[3]!) };\n\tif (!computed) return { r: 0, g: 0, b: 0 };\n\ttry {\n\t\tconst probe = document.createElement(\"canvas\");\n\t\tprobe.width = probe.height = 1;\n\t\tconst pctx = probe.getContext(\"2d\", { willReadFrequently: true });\n\t\tif (pctx) {\n\t\t\tpctx.fillStyle = computed;\n\t\t\tpctx.fillRect(0, 0, 1, 1);\n\t\t\tconst [r = 0, g = 0, b = 0] = pctx.getImageData(0, 0, 1, 1).data;\n\t\t\treturn { r, g, b };\n\t\t}\n\t} catch {\n\t\t// fall through to the black fallback\n\t}\n\treturn { r: 0, g: 0, b: 0 };\n}\n\n/**\n * Colour resolver: a hex string is parsed once per value, `currentColor` is\n * re-read from `el`'s computed style on every call (so a light/dark switch\n * lands on the next frame) and re-parsed only when that string changes.\n */\nfunction createColorResolver(el: Element) {\n\tlet lastComputed = \"\";\n\tlet lastRgb: Rgb = { r: 0, g: 0, b: 0 };\n\tlet lastHex = \"\";\n\tlet lastHexRgb: Rgb = { r: 0, g: 0, b: 0 };\n\treturn (color: string): Rgb => {\n\t\tif (CURRENT_COLOR.test(color)) {\n\t\t\tconst computed = getComputedStyle(el).color;\n\t\t\tif (computed !== lastComputed) {\n\t\t\t\tlastComputed = computed;\n\t\t\t\tlastRgb = computedToRgb(computed);\n\t\t\t}\n\t\t\treturn lastRgb;\n\t\t}\n\t\tif (color !== lastHex) {\n\t\t\tlastHex = color;\n\t\t\tlastHexRgb = parseHex(color);\n\t\t}\n\t\treturn lastHexRgb;\n\t};\n}\n\nexport function FlickeringGrid({\n\tsquareSize = 4,\n\tgridGap = 6,\n\tflickerChance = 0.3,\n\tcolor = \"currentColor\",\n\tmaxOpacity = 0.3,\n\twidth,\n\theight,\n\tclassName,\n\tseed = 1,\n}: FlickeringGridProps) {\n\tconst [container, containerRef] = useElementRef<HTMLDivElement>();\n\tconst [canvas, canvasRef] = useElementRef<HTMLCanvasElement>();\n\n\t// Read inside the running loop only, never as effect dependencies. The\n\t// source runs the whole lifecycle from `onMount`, so nothing it reads is\n\t// tracked: a new `color` or `flickerChance` lands on the next frame, and a\n\t// new `squareSize` / `gridGap` / `maxOpacity` / `width` / `height` only\n\t// takes effect at the next `setupCanvas` — mount or resize. Making any of\n\t// them a dependency would tear the canvas down and re-seed every square on\n\t// a colour change, which the source never does.\n\tconst squareSizeRef = useLiveRef(squareSize);\n\tconst gridGapRef = useLiveRef(gridGap);\n\tconst flickerChanceRef = useLiveRef(flickerChance);\n\tconst colorRef = useLiveRef(color);\n\tconst maxOpacityRef = useLiveRef(maxOpacity);\n\tconst widthRef = useLiveRef(width);\n\tconst heightRef = useLiveRef(height);\n\n\t// `seed` IS a dependency: it defines the stream from its first value on, so\n\t// changing it has to restart the grid.\n\tuseEffect(() => {\n\t\tif (!container || !canvas) return;\n\n\t\tconst context = canvas.getContext(\"2d\");\n\t\tif (!context) return;\n\t\t// Re-bound with explicit non-null types: the drawing helpers below are\n\t\t// hoisted function declarations, and TypeScript does not carry the null\n\t\t// checks above into one.\n\t\tconst ctx: CanvasRenderingContext2D = context;\n\t\tconst containerEl: HTMLDivElement = container;\n\t\tconst canvasEl: HTMLCanvasElement = canvas;\n\n\t\tconst random = mulberry32(seed);\n\t\tconst resolveColor = createColorResolver(containerEl);\n\n\t\tlet isInView = false;\n\t\tlet animationFrameId: number | undefined;\n\t\tlet lastTime = 0;\n\t\tlet cols = 0;\n\t\tlet rows = 0;\n\t\tlet squares = new Float32Array(0);\n\t\tlet dpr = 1;\n\n\t\tfunction setupCanvas(w: number, h: number) {\n\t\t\tconst squareSize = squareSizeRef.current;\n\t\t\tconst gridGap = gridGapRef.current;\n\t\t\tconst maxOpacity = maxOpacityRef.current;\n\n\t\t\tdpr = window.devicePixelRatio || 1;\n\t\t\tcanvasEl.width = w * dpr;\n\t\t\tcanvasEl.height = h * dpr;\n\t\t\tcanvasEl.style.width = `${w}px`;\n\t\t\tcanvasEl.style.height = `${h}px`;\n\n\t\t\tcols = Math.floor(w / (squareSize + gridGap));\n\t\t\trows = Math.floor(h / (squareSize + gridGap));\n\n\t\t\tsquares = new Float32Array(cols * rows);\n\t\t\tfor (let i = 0; i < squares.length; i++) {\n\t\t\t\tsquares[i] = random() * maxOpacity;\n\t\t\t}\n\t\t}\n\n\t\tfunction updateSquares(deltaTime: number) {\n\t\t\tconst flickerChance = flickerChanceRef.current;\n\t\t\tconst maxOpacity = maxOpacityRef.current;\n\n\t\t\tfor (let i = 0; i < squares.length; i++) {\n\t\t\t\tif (random() < flickerChance * deltaTime) {\n\t\t\t\t\tsquares[i] = random() * maxOpacity;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction drawGrid() {\n\t\t\tconst squareSize = squareSizeRef.current;\n\t\t\tconst gridGap = gridGapRef.current;\n\n\t\t\tconst { r, g, b } = resolveColor(colorRef.current);\n\t\t\tconst colorPrefix = `rgba(${r}, ${g}, ${b},`;\n\t\t\tctx.clearRect(0, 0, canvasEl.width, canvasEl.height);\n\t\t\tfor (let i = 0; i < cols; i++) {\n\t\t\t\tfor (let j = 0; j < rows; j++) {\n\t\t\t\t\tconst opacity = squares[i * rows + j];\n\t\t\t\t\tctx.fillStyle = `${colorPrefix}${opacity})`;\n\t\t\t\t\tctx.fillRect(\n\t\t\t\t\t\ti * (squareSize + gridGap) * dpr,\n\t\t\t\t\t\tj * (squareSize + gridGap) * dpr,\n\t\t\t\t\t\tsquareSize * dpr,\n\t\t\t\t\t\tsquareSize * dpr\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction updateCanvasSize() {\n\t\t\tconst w = widthRef.current ?? containerEl.clientWidth;\n\t\t\tconst h = heightRef.current ?? containerEl.clientHeight;\n\t\t\tsetupCanvas(w, h);\n\t\t}\n\n\t\tfunction animate(time: number) {\n\t\t\tif (!isInView) return;\n\n\t\t\tconst deltaTime = (time - lastTime) / 1000;\n\t\t\tlastTime = time;\n\n\t\t\tupdateSquares(deltaTime);\n\t\t\tdrawGrid();\n\t\t\tanimationFrameId = requestAnimationFrame(animate);\n\t\t}\n\n\t\tupdateCanvasSize();\n\n\t\tconst resizeObserver = new ResizeObserver(() => {\n\t\t\tupdateCanvasSize();\n\t\t});\n\n\t\tconst intersectionObserver = new IntersectionObserver(\n\t\t\t(entries) => {\n\t\t\t\t// `([entry]) =>` in the source; split in two under\n\t\t\t\t// `noUncheckedIndexedAccess`, which types the first element as\n\t\t\t\t// possibly absent. The callback never fires with an empty list.\n\t\t\t\tconst entry = entries[0];\n\t\t\t\tif (!entry) return;\n\n\t\t\t\tisInView = entry.isIntersecting;\n\t\t\t\tif (isInView) {\n\t\t\t\t\tanimationFrameId = requestAnimationFrame(animate);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ threshold: 0 }\n\t\t);\n\n\t\tresizeObserver.observe(containerEl);\n\t\tintersectionObserver.observe(canvasEl);\n\n\t\treturn () => {\n\t\t\tif (animationFrameId) cancelAnimationFrame(animationFrameId);\n\t\t\tresizeObserver.disconnect();\n\t\t\tintersectionObserver.disconnect();\n\t\t};\n\t}, [\n\t\tcontainer,\n\t\tcanvas,\n\t\tseed,\n\t\tsquareSizeRef,\n\t\tgridGapRef,\n\t\tflickerChanceRef,\n\t\tcolorRef,\n\t\tmaxOpacityRef,\n\t\twidthRef,\n\t\theightRef,\n\t]);\n\n\treturn (\n\t\t<div ref={containerRef} className={cn(\"h-full w-full\", className)}>\n\t\t\t<canvas ref={canvasRef} className=\"pointer-events-none\" />\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/flickering-grid/FlickeringGrid.tsx"},{"path":"react/src/components/flickering-grid/index.ts","content":"\"use client\";\n\nexport { FlickeringGrid } from \"./FlickeringGrid\";\nexport type { FlickeringGridProps } from \"./FlickeringGrid\";\n","type":"registry:ui","target":"@ui/fancy/components/flickering-grid/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/flickering-grid?framework=react"}}