{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flickering-grid",
  "title": "Flickering Grid",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A canvas grid of small squares that flicker at random brightness, like a busy status board. Pixel-snapped, token-colored, DPR aware, pauses off-screen, static under reduced motion.",
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/color.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/flickering-grid.tsx",
      "content": "// Ballmac UI: Flickering Grid. https://ui.ballmac.com/components/flickering-grid\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { observeTheme, resolveCssColor } from \"@/lib/ballmac/color\"\n\ntype FlickeringGridProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"color\"> & {\n  /** Side of each square in pixels. */\n  squareSize?: number\n  /** Space between squares in pixels. */\n  gap?: number\n  /** Chance per second that a square picks a new brightness (0–1+). */\n  flickerChance?: number\n  /** Brightest a square gets (0–1). */\n  maxOpacity?: number\n  /** Square color: a CSS variable name (\"--foreground\") or any CSS color. */\n  color?: string\n  /** Fade the grid out toward the edges with a radial mask. */\n  fade?: boolean\n}\n\nfunction FlickeringGrid({\n  squareSize = 4,\n  gap = 6,\n  flickerChance = 0.3,\n  maxOpacity = 0.3,\n  color = \"--foreground\",\n  fade = true,\n  className,\n  style,\n  ...props\n}: FlickeringGridProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    if (!root || !canvas || !ctx) return\n    const reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    const pitch = squareSize + gap\n\n    let width = 0\n    let height = 0\n    let cols = 0\n    let rows = 0\n    let dpr = 1\n    let opacities = new Float32Array(0)\n    let fill = resolveCssColor(root, color)\n\n    const resize = () => {\n      dpr = Math.min(window.devicePixelRatio || 1, 2)\n      width = root.clientWidth\n      height = root.clientHeight\n      canvas.width = Math.round(width * dpr)\n      canvas.height = Math.round(height * dpr)\n      canvas.style.width = `${width}px`\n      canvas.style.height = `${height}px`\n      cols = Math.ceil(width / pitch)\n      rows = Math.ceil(height / pitch)\n      opacities = new Float32Array(cols * rows)\n      for (let i = 0; i < opacities.length; i++) opacities[i] = Math.random() * maxOpacity\n    }\n\n    const draw = () => {\n      // Draw in device pixels and snap squares to whole pixels so they stay crisp.\n      ctx.setTransform(1, 0, 0, 1, 0, 0)\n      ctx.clearRect(0, 0, canvas.width, canvas.height)\n      ctx.fillStyle = fill\n      const size = Math.max(1, Math.round(squareSize * dpr))\n      for (let c = 0; c < cols; c++) {\n        const x = Math.round(c * pitch * dpr)\n        for (let r = 0; r < rows; r++) {\n          ctx.globalAlpha = opacities[c * rows + r]!\n          ctx.fillRect(x, Math.round(r * pitch * dpr), size, size)\n        }\n      }\n      ctx.globalAlpha = 1\n    }\n\n    let frame = 0\n    let last = 0\n    let visible = true\n    const loop = (now: number) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.1) : 0\n      last = now\n      const chance = flickerChance * dt\n      for (let i = 0; i < opacities.length; i++) {\n        if (Math.random() < chance) opacities[i] = Math.random() * maxOpacity\n      }\n      draw()\n      frame = requestAnimationFrame(loop)\n    }\n    const sync = () => {\n      cancelAnimationFrame(frame)\n      last = 0\n      if (reduceMotion) draw()\n      else if (visible && !document.hidden) frame = requestAnimationFrame(loop)\n    }\n\n    resize()\n    sync()\n    const ro = new ResizeObserver(() => {\n      resize()\n      draw()\n    })\n    ro.observe(root)\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? true\n      sync()\n    })\n    io.observe(root)\n    const stopTheme = observeTheme(() => {\n      fill = resolveCssColor(root, color)\n      draw()\n    })\n    document.addEventListener(\"visibilitychange\", sync)\n    return () => {\n      cancelAnimationFrame(frame)\n      ro.disconnect()\n      io.disconnect()\n      stopTheme()\n      document.removeEventListener(\"visibilitychange\", sync)\n    }\n  }, [squareSize, gap, flickerChance, maxOpacity, color])\n\n  const mask = fade ? \"radial-gradient(ellipse 70% 65% at 50% 50%, black 30%, transparent 80%)\" : undefined\n\n  return (\n    <div\n      ref={rootRef}\n      aria-hidden=\"true\"\n      data-slot=\"flickering-grid\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      style={{ maskImage: mask, WebkitMaskImage: mask, ...style }}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block\" />\n    </div>\n  )\n}\n\nexport { FlickeringGrid, type FlickeringGridProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/flickering-grid.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "grid",
      "pixels",
      "flicker",
      "canvas",
      "background",
      "hero"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Place <FlickeringGrid /> as the first child of a relative, overflow-hidden container; it fills it (absolute inset-0, aria-hidden, pointer-events-none). Tune squareSize, gap and maxOpacity for density and contrast.",
      "whenToUse": [
        "Section or hero backgrounds for infrastructure, data or developer products",
        "Feature cards that should feel alive without a focal animation"
      ],
      "whenNotToUse": [
        "Behind small or low-contrast text (keep maxOpacity low or add a scrim)",
        "Very large full-page areas on low-power devices (use dot-pattern)"
      ],
      "composesWith": [
        "number-ticker",
        "shimmer-text"
      ],
      "a11y": [],
      "customization": [
        "squareSize (px, default 4), gap (px, default 6)",
        "flickerChance: changes per square per second (default 0.3); maxOpacity (default 0.3)",
        "color: CSS variable name or color, default --foreground; fade toggles the radial mask"
      ]
    },
    "examples": [
      "flickering-grid-demo",
      "flickering-grid-accent"
    ],
    "url": "https://ui.ballmac.com/components/flickering-grid"
  },
  "categories": [
    "backgrounds"
  ],
  "type": "registry:ui"
}