{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flickering-grid",
  "type": "registry:ui",
  "title": "Flickering Grid",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "High-performance HTML5 canvas flickering retro grid background with device-pixel-ratio scaling, probabilistic opacity stepping, and automatic offscreen pausing via IntersectionObserver.",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/flickering-grid.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface FlickeringGridProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  /** Size of each square grid cell in pixels. Default: 4 */\n  squareSize?: number;\n  /** Gap between adjacent grid squares in pixels. Default: 6 */\n  gridGap?: number;\n  /** Probability of each square updating its opacity per second. Default: 0.3 */\n  flickerChance?: number;\n  /** CSS color string for the grid squares. Default: \"rgb(0, 0, 0)\" */\n  color?: string;\n  /** Explicit fixed canvas width (optional; defaults to container width) */\n  width?: number;\n  /** Explicit fixed canvas height (optional; defaults to container height) */\n  height?: number;\n  /** Maximum opacity ceiling for the grid squares [0..1]. Default: 0.3 */\n  maxOpacity?: number;\n  /** Additional Tailwind CSS classes */\n  className?: string;\n}\n\n/**\n * Fast, SSR-safe color parser converting hex, rgb, or color strings to an RGBA prefix \"rgba(r, g, b, \".\n */\nfunction parseColorToRgbaPrefix(colorStr: string): string {\n  if (typeof window === \"undefined\") {\n    return \"rgba(0, 0, 0, \";\n  }\n\n  // Fast-path for hex (#fff, #ffffff, #ffffff80)\n  if (colorStr.startsWith(\"#\")) {\n    let hex = colorStr.slice(1);\n    if (hex.length === 3) {\n      hex = hex\n        .split(\"\")\n        .map((c) => c + c)\n        .join(\"\");\n    }\n    const num = parseInt(hex.slice(0, 6), 16);\n    if (!Number.isNaN(num)) {\n      const r = (num >> 16) & 255;\n      const g = (num >> 8) & 255;\n      const b = num & 255;\n      return `rgba(${r}, ${g}, ${b}, `;\n    }\n  }\n\n  // Fast-path for rgb(r, g, b) or rgba(r, g, b, a)\n  const rgbMatch = colorStr.match(/rgba?\\((\\d+)[,\\s]+(\\d+)[,\\s]+(\\d+)/i);\n  if (rgbMatch) {\n    return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, `;\n  }\n\n  // Fallback to DOM 1x1 canvas for named colors (e.g. \"currentColor\", \"tomato\", \"blue\")\n  try {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = canvas.height = 1;\n    const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n    if (!ctx) return \"rgba(0, 0, 0, \";\n    ctx.fillStyle = colorStr;\n    ctx.fillRect(0, 0, 1, 1);\n    const data = ctx.getImageData(0, 0, 1, 1).data;\n    return `rgba(${data[0]}, ${data[1]}, ${data[2]}, `;\n  } catch {\n    return \"rgba(0, 0, 0, \";\n  }\n}\n\nexport const FlickeringGrid: React.FC<FlickeringGridProps> = ({\n  squareSize = 4,\n  gridGap = 6,\n  flickerChance = 0.3,\n  color = \"rgb(0, 0, 0)\",\n  width,\n  height,\n  className,\n  maxOpacity = 0.3,\n  ...props\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isInView, setIsInView] = useState(false);\n  const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });\n\n  const memoizedColorPrefix = useMemo(() => {\n    return parseColorToRgbaPrefix(color);\n  }, [color]);\n\n  const setupCanvas = useCallback(\n    (canvas: HTMLCanvasElement, w: number, h: number) => {\n      if (w <= 0 || h <= 0) return null;\n      const dpr = typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n      canvas.width = Math.floor(w * dpr);\n      canvas.height = Math.floor(h * dpr);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n\n      const cellSize = squareSize + gridGap;\n      const cols = Math.ceil(w / cellSize);\n      const rows = Math.ceil(h / cellSize);\n\n      const squares = new Float32Array(cols * rows);\n      for (let i = 0; i < squares.length; i++) {\n        squares[i] = Math.random() * maxOpacity;\n      }\n\n      return { cols, rows, squares, dpr };\n    },\n    [squareSize, gridGap, maxOpacity]\n  );\n\n  const updateSquares = useCallback(\n    (squares: Float32Array, deltaTime: number) => {\n      const stepChance = flickerChance * deltaTime;\n      for (let i = 0; i < squares.length; i++) {\n        if (Math.random() < stepChance) {\n          squares[i] = Math.random() * maxOpacity;\n        }\n      }\n    },\n    [flickerChance, maxOpacity]\n  );\n\n  const drawGrid = useCallback(\n    (\n      ctx: CanvasRenderingContext2D,\n      w: number,\n      h: number,\n      cols: number,\n      rows: number,\n      squares: Float32Array,\n      dpr: number\n    ) => {\n      ctx.clearRect(0, 0, w, h);\n\n      const cellSize = (squareSize + gridGap) * dpr;\n      const size = squareSize * dpr;\n\n      for (let i = 0; i < cols; i++) {\n        const x = Math.floor(i * cellSize);\n        for (let j = 0; j < rows; j++) {\n          const idx = i * rows + j;\n          if (idx >= squares.length) continue;\n          const opacity = squares[idx];\n          ctx.fillStyle = `${memoizedColorPrefix}${opacity})`;\n          const y = Math.floor(j * cellSize);\n          ctx.fillRect(x, y, size, size);\n        }\n      }\n    },\n    [memoizedColorPrefix, squareSize, gridGap]\n  );\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext(\"2d\", { alpha: true });\n    if (!ctx) return;\n\n    let animationFrameId: number | null = null;\n    let resizeObserver: ResizeObserver | null = null;\n    let intersectionObserver: IntersectionObserver | null = null;\n    let gridParams: ReturnType<typeof setupCanvas> = null;\n\n    const updateCanvasSize = () => {\n      const newWidth = width || container.clientWidth;\n      const newHeight = height || container.clientHeight;\n      if (newWidth <= 0 || newHeight <= 0) return;\n      setCanvasSize({ width: newWidth, height: newHeight });\n      gridParams = setupCanvas(canvas, newWidth, newHeight);\n    };\n\n    updateCanvasSize();\n\n    let lastTime = performance.now();\n    const animate = (time: number) => {\n      if (!isInView || !gridParams) return;\n\n      const deltaTime = Math.min((time - lastTime) / 1000, 0.1); // clamp deltaTime to prevent huge jumps on tab resume\n      lastTime = time;\n\n      updateSquares(gridParams.squares, deltaTime);\n      drawGrid(\n        ctx,\n        canvas.width,\n        canvas.height,\n        gridParams.cols,\n        gridParams.rows,\n        gridParams.squares,\n        gridParams.dpr\n      );\n      animationFrameId = requestAnimationFrame(animate);\n    };\n\n    resizeObserver = new ResizeObserver(() => {\n      updateCanvasSize();\n    });\n    resizeObserver.observe(container);\n\n    intersectionObserver = new IntersectionObserver(\n      ([entry]) => {\n        setIsInView(entry.isIntersecting);\n      },\n      { threshold: 0 }\n    );\n    intersectionObserver.observe(container);\n\n    if (isInView && gridParams) {\n      animationFrameId = requestAnimationFrame(animate);\n    }\n\n    return () => {\n      if (animationFrameId !== null) {\n        cancelAnimationFrame(animationFrameId);\n      }\n      if (resizeObserver) {\n        resizeObserver.disconnect();\n      }\n      if (intersectionObserver) {\n        intersectionObserver.disconnect();\n      }\n    };\n  }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"h-full w-full overflow-hidden\", className)}\n      {...props}\n    >\n      <canvas\n        ref={canvasRef}\n        className=\"pointer-events-none block\"\n        style={{\n          width: canvasSize.width,\n          height: canvasSize.height,\n        }}\n      />\n    </div>\n  );\n};\n\nexport default FlickeringGrid;\n",
      "type": "registry:ui",
      "target": "components/ui/flickering-grid.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
