{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "flickering-grid", "title": "Flickering Grid", "description": "A grid that flickers with a subtle animation.", "dependencies": [ "motion" ], "registryDependencies": [ "amitgajare2/ariseui/utils" ], "files": [ { "path": "components/ui/flickering-grid.tsx", "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ntype FlickeringGridProps = Omit<\r\n React.ComponentPropsWithoutRef<\"div\">,\r\n \"color\" | \"children\"\r\n> & {\r\n /** Side length of each square in CSS pixels. */\r\n squareSize?: number;\r\n /** Space between squares in CSS pixels. */\r\n gridGap?: number;\r\n /** Approximate chance per second that a square receives a new opacity. */\r\n flickerChance?: number;\r\n /** Any browser-resolvable CSS color, including CSS variables. */\r\n color?: string;\r\n /** Fixed canvas width. Uses the container width when omitted. */\r\n width?: number;\r\n /** Fixed canvas height. Uses the container height when omitted. */\r\n height?: number;\r\n /** Maximum opacity assigned to an individual square. */\r\n maxOpacity?: number;\r\n /** Frames rendered per second. Lower values use less CPU. */\r\n fps?: number;\r\n /** Maximum device-pixel ratio used by the canvas. */\r\n pixelRatio?: number;\r\n /** Pause animation when the user requests reduced motion. */\r\n respectReducedMotion?: boolean;\r\n};\r\n\r\ntype GridState = {\r\n width: number;\r\n height: number;\r\n columns: number;\r\n rows: number;\r\n opacities: Float32Array;\r\n dpr: number;\r\n};\r\n\r\nconst EMPTY_GRID: GridState = {\r\n width: 0,\r\n height: 0,\r\n columns: 0,\r\n rows: 0,\r\n opacities: new Float32Array(0),\r\n dpr: 1,\r\n};\r\n\r\nfunction clamp(value: number, minimum: number, maximum: number) {\r\n return Math.min(Math.max(value, minimum), maximum);\r\n}\r\n\r\nfunction resolveCssColor(color: string, element: HTMLElement) {\r\n const probe = document.createElement(\"span\");\r\n probe.style.color = color;\r\n probe.style.display = \"none\";\r\n element.appendChild(probe);\r\n\r\n const resolvedColor = window.getComputedStyle(probe).color;\r\n probe.remove();\r\n\r\n return resolvedColor || color;\r\n}\r\n\r\nconst FlickeringGrid = React.forwardRef(\r\n (\r\n {\r\n squareSize = 4,\r\n gridGap = 6,\r\n flickerChance = 0.3,\r\n color = \"rgb(0 0 0)\",\r\n width,\r\n height,\r\n maxOpacity = 0.3,\r\n fps = 30,\r\n pixelRatio = 2,\r\n respectReducedMotion = true,\r\n className,\r\n style,\r\n ...props\r\n },\r\n forwardedRef,\r\n ) => {\r\n const containerRef = React.useRef(null);\r\n const canvasRef = React.useRef(null);\r\n const gridRef = React.useRef(EMPTY_GRID);\r\n const isVisibleRef = React.useRef(true);\r\n const isDocumentVisibleRef = React.useRef(true);\r\n const animationFrameRef = React.useRef(null);\r\n\r\n const setContainerRef = React.useCallback(\r\n (node: HTMLDivElement | null) => {\r\n containerRef.current = node;\r\n\r\n if (typeof forwardedRef === \"function\") {\r\n forwardedRef(node);\r\n } else if (forwardedRef) {\r\n forwardedRef.current = node;\r\n }\r\n },\r\n [forwardedRef],\r\n );\r\n\r\n const safeSquareSize = Math.max(1, squareSize);\r\n const safeGridGap = Math.max(0, gridGap);\r\n const safeFlickerChance = Math.max(0, flickerChance);\r\n const safeMaxOpacity = clamp(maxOpacity, 0, 1);\r\n const safeFps = clamp(fps, 1, 120);\r\n const safePixelRatio = Math.max(1, pixelRatio);\r\n\r\n React.useEffect(() => {\r\n const container = containerRef.current;\r\n const canvas = canvasRef.current;\r\n\r\n if (!container || !canvas) return;\r\n\r\n const context = canvas.getContext(\"2d\", { alpha: true });\r\n if (!context) return;\r\n\r\n let disposed = false;\r\n let lastFrameTime = 0;\r\n let lastUpdateTime = 0;\r\n let reducedMotionQuery: MediaQueryList | null = null;\r\n let prefersReducedMotion = false;\r\n\r\n const frameInterval = 1000 / safeFps;\r\n const cellSize = safeSquareSize + safeGridGap;\r\n const resolvedColor = resolveCssColor(color, container);\r\n\r\n const draw = () => {\r\n const grid = gridRef.current;\r\n\r\n context.clearRect(0, 0, canvas.width, canvas.height);\r\n context.fillStyle = resolvedColor;\r\n\r\n for (let column = 0; column < grid.columns; column += 1) {\r\n const x = column * cellSize;\r\n\r\n for (let row = 0; row < grid.rows; row += 1) {\r\n const opacity = grid.opacities[column * grid.rows + row];\r\n if (opacity <= 0) continue;\r\n\r\n context.globalAlpha = opacity;\r\n context.fillRect(x, row * cellSize, safeSquareSize, safeSquareSize);\r\n }\r\n }\r\n\r\n context.globalAlpha = 1;\r\n };\r\n\r\n const createGrid = () => {\r\n const nextWidth = Math.max(0, Math.round(width ?? container.clientWidth));\r\n const nextHeight = Math.max(\r\n 0,\r\n Math.round(height ?? container.clientHeight),\r\n );\r\n const dpr = Math.min(window.devicePixelRatio || 1, safePixelRatio);\r\n const columns = Math.ceil(nextWidth / cellSize);\r\n const rows = Math.ceil(nextHeight / cellSize);\r\n const opacities = new Float32Array(columns * rows);\r\n\r\n for (let index = 0; index < opacities.length; index += 1) {\r\n opacities[index] = Math.random() * safeMaxOpacity;\r\n }\r\n\r\n canvas.width = Math.max(1, Math.round(nextWidth * dpr));\r\n canvas.height = Math.max(1, Math.round(nextHeight * dpr));\r\n canvas.style.width = `${nextWidth}px`;\r\n canvas.style.height = `${nextHeight}px`;\r\n\r\n context.setTransform(dpr, 0, 0, dpr, 0, 0);\r\n\r\n gridRef.current = {\r\n width: nextWidth,\r\n height: nextHeight,\r\n columns,\r\n rows,\r\n opacities,\r\n dpr,\r\n };\r\n\r\n draw();\r\n };\r\n\r\n const updateGrid = (deltaSeconds: number) => {\r\n const { opacities } = gridRef.current;\r\n const probability = clamp(safeFlickerChance * deltaSeconds, 0, 1);\r\n\r\n for (let index = 0; index < opacities.length; index += 1) {\r\n if (Math.random() < probability) {\r\n opacities[index] = Math.random() * safeMaxOpacity;\r\n }\r\n }\r\n };\r\n\r\n const shouldAnimate = () =>\r\n isVisibleRef.current &&\r\n isDocumentVisibleRef.current &&\r\n !(respectReducedMotion && prefersReducedMotion);\r\n\r\n const animate = (time: number) => {\r\n if (disposed) return;\r\n\r\n animationFrameRef.current = window.requestAnimationFrame(animate);\r\n\r\n if (!shouldAnimate()) {\r\n lastFrameTime = time;\r\n lastUpdateTime = time;\r\n return;\r\n }\r\n\r\n if (time - lastFrameTime < frameInterval) return;\r\n\r\n const deltaSeconds = Math.min(\r\n Math.max((time - (lastUpdateTime || time)) / 1000, 0),\r\n 0.25,\r\n );\r\n\r\n lastFrameTime = time;\r\n lastUpdateTime = time;\r\n\r\n updateGrid(deltaSeconds);\r\n draw();\r\n };\r\n\r\n const resizeObserver = new ResizeObserver(createGrid);\r\n resizeObserver.observe(container);\r\n\r\n const intersectionObserver = new IntersectionObserver(\r\n ([entry]) => {\r\n isVisibleRef.current = entry?.isIntersecting ?? true;\r\n },\r\n { threshold: 0 },\r\n );\r\n intersectionObserver.observe(container);\r\n\r\n const handleVisibilityChange = () => {\r\n isDocumentVisibleRef.current = document.visibilityState === \"visible\";\r\n };\r\n document.addEventListener(\"visibilitychange\", handleVisibilityChange);\r\n\r\n if (respectReducedMotion) {\r\n reducedMotionQuery = window.matchMedia(\r\n \"(prefers-reduced-motion: reduce)\",\r\n );\r\n prefersReducedMotion = reducedMotionQuery.matches;\r\n\r\n const handleReducedMotionChange = (event: MediaQueryListEvent) => {\r\n prefersReducedMotion = event.matches;\r\n draw();\r\n };\r\n\r\n reducedMotionQuery.addEventListener(\r\n \"change\",\r\n handleReducedMotionChange,\r\n );\r\n\r\n createGrid();\r\n animationFrameRef.current = window.requestAnimationFrame(animate);\r\n\r\n return () => {\r\n disposed = true;\r\n resizeObserver.disconnect();\r\n intersectionObserver.disconnect();\r\n document.removeEventListener(\r\n \"visibilitychange\",\r\n handleVisibilityChange,\r\n );\r\n reducedMotionQuery?.removeEventListener(\r\n \"change\",\r\n handleReducedMotionChange,\r\n );\r\n\r\n if (animationFrameRef.current !== null) {\r\n window.cancelAnimationFrame(animationFrameRef.current);\r\n }\r\n };\r\n }\r\n\r\n createGrid();\r\n animationFrameRef.current = window.requestAnimationFrame(animate);\r\n\r\n return () => {\r\n disposed = true;\r\n resizeObserver.disconnect();\r\n intersectionObserver.disconnect();\r\n document.removeEventListener(\r\n \"visibilitychange\",\r\n handleVisibilityChange,\r\n );\r\n\r\n if (animationFrameRef.current !== null) {\r\n window.cancelAnimationFrame(animationFrameRef.current);\r\n }\r\n };\r\n }, [\r\n color,\r\n height,\r\n respectReducedMotion,\r\n safeFlickerChance,\r\n safeFps,\r\n safeGridGap,\r\n safeMaxOpacity,\r\n safePixelRatio,\r\n safeSquareSize,\r\n width,\r\n ]);\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n },\r\n);\r\n\r\nFlickeringGrid.displayName = \"FlickeringGrid\";\r\n\r\nexport { FlickeringGrid };\r\nexport type { FlickeringGridProps };\r\n", "type": "registry:ui" } ], "type": "registry:ui" }