{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-grid",
  "title": "Animated Grid",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A decorative hairline grid background where a few random cells softly light up and fade, like instrument lights. SVG, hydration-safe, static under reduced motion.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/animated-grid.tsx",
      "content": "// Ballmac UI: Animated Grid. https://ui.ballmac.com/components/animated-grid\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ease } from \"@/lib/ballmac/motion\"\n\ntype AnimatedGridProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Width and height of one cell in pixels. */\n  cellSize?: number\n  /** How many cells are lit at the same time. */\n  count?: number\n  /** Milliseconds between one cell fading out and a new one lighting up. */\n  interval?: number\n  /** Fade the grid out toward the edges with a radial mask. */\n  fade?: boolean\n}\n\ntype Cell = { id: number; col: number; row: number }\n\nfunction AnimatedGrid({\n  cellSize = 40,\n  count = 8,\n  interval = 700,\n  fade = true,\n  className,\n  style,\n  ...props\n}: AnimatedGridProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const patternId = `bm-grid-${React.useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`\n  const reduceMotion = useReducedMotion()\n  const [grid, setGrid] = React.useState({ cols: 0, rows: 0 })\n  // Lit cells are chosen on the client only, after mount, so server and client HTML match.\n  const [cells, setCells] = React.useState<Cell[]>([])\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n    const measure = () => {\n      const cols = Math.ceil(root.clientWidth / cellSize)\n      const rows = Math.ceil(root.clientHeight / cellSize)\n      setGrid((prev) => (prev.cols === cols && prev.rows === rows ? prev : { cols, rows }))\n    }\n    measure()\n    const observer = new ResizeObserver(measure)\n    observer.observe(root)\n    return () => observer.disconnect()\n  }, [cellSize])\n\n  React.useEffect(() => {\n    const { cols, rows } = grid\n    if (reduceMotion || cols === 0 || rows === 0 || count <= 0) {\n      setCells([])\n      return\n    }\n    let nextId = 0\n    const pick = (taken: Cell[]): Cell => {\n      for (let attempt = 0; attempt < 12; attempt++) {\n        const col = Math.floor(Math.random() * cols)\n        const row = Math.floor(Math.random() * rows)\n        if (!taken.some((c) => c.col === col && c.row === row)) return { id: nextId++, col, row }\n      }\n      return { id: nextId++, col: Math.floor(Math.random() * cols), row: Math.floor(Math.random() * rows) }\n    }\n    // Start empty and add one cell per tick; once full, the oldest makes room for a new one.\n    setCells([])\n    const timer = window.setInterval(() => {\n      setCells((prev) => {\n        const kept = prev.length >= count ? prev.slice(prev.length - count + 1) : prev\n        return [...kept, pick(kept)]\n      })\n    }, interval)\n    return () => window.clearInterval(timer)\n  }, [grid, count, interval, reduceMotion])\n\n  // Each cell lives for count × interval; it has fully faded out just before it is replaced.\n  const life = (count * interval * 0.95) / 1000\n  const mask = fade ? \"radial-gradient(ellipse 75% 70% at 50% 45%, black 35%, transparent 85%)\" : undefined\n\n  return (\n    <div\n      ref={rootRef}\n      aria-hidden=\"true\"\n      data-slot=\"animated-grid\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 overflow-hidden [--grid-light:color-mix(in_oklch,var(--ring)_20%,transparent)] [--grid-line:color-mix(in_oklch,var(--foreground)_8%,transparent)]\",\n        className\n      )}\n      style={{ maskImage: mask, WebkitMaskImage: mask, ...style }}\n      {...props}\n    >\n      <svg className=\"absolute inset-0 size-full\">\n        <defs>\n          <pattern id={patternId} width={cellSize} height={cellSize} patternUnits=\"userSpaceOnUse\">\n            <path\n              d={`M ${cellSize} 0.5 H 0.5 V ${cellSize}`}\n              fill=\"none\"\n              stroke=\"var(--grid-line)\"\n              strokeWidth={1}\n              shapeRendering=\"crispEdges\"\n            />\n          </pattern>\n        </defs>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${patternId})`} />\n        {cells.map((cell) => (\n          <motion.rect\n            key={cell.id}\n            data-slot=\"animated-grid-cell\"\n            x={cell.col * cellSize + 1}\n            y={cell.row * cellSize + 1}\n            width={cellSize - 1}\n            height={cellSize - 1}\n            fill=\"var(--grid-light)\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: [0, 1, 1, 0] }}\n            transition={{ duration: life, times: [0, 0.2, 0.7, 1], ease: ease.inOut }}\n          />\n        ))}\n      </svg>\n    </div>\n  )\n}\n\nexport { AnimatedGrid, type AnimatedGridProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/animated-grid.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "background",
      "grid",
      "pattern",
      "hero",
      "decorative",
      "motion"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "Place <AnimatedGrid /> as the first child of a relative container (it is absolute inset-0, aria-hidden, pointer-events-none) and put content after it with relative positioning. Lit cells are picked on the client after mount, so SSR output is just the grid.",
      "whenToUse": [
        "Hero or section backgrounds on developer-tool and SaaS landing pages",
        "Empty states or sign-in screens that need quiet texture"
      ],
      "whenNotToUse": [
        "Behind dense text or data (it competes with content)",
        "More than one per screen"
      ],
      "composesWith": [
        "text-reveal",
        "shimmer-text",
        "magnetic-button"
      ],
      "a11y": [],
      "customization": [
        "cellSize in px (default 40); count of lit cells (default 8); interval in ms between changes (default 700)",
        "fade toggles the radial edge mask; pass your own mask via style",
        "Colors are CSS variables: className=\"[--grid-line:…] [--grid-light:…]\" (defaults: 8% foreground lines, 20% ring cells)"
      ]
    },
    "examples": [
      "animated-grid-demo"
    ],
    "url": "https://ui.ballmac.com/components/animated-grid"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}