{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grid-backdrop",
  "type": "registry:ui",
  "title": "Grid Backdrop",
  "description": "An SVG grid with cells that fade in and out at a deterministic, seeded scatter.",
  "registryDependencies": [
    "utils",
    "use-in-viewport"
  ],
  "files": [
    {
      "path": "registry/loomui/grid-backdrop.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useInViewport } from \"@/registry/lib/use-in-viewport\"\n\nexport interface GridBackdropProps extends React.ComponentProps<\"svg\"> {\n  /** Cell width in pixels. */\n  width?: number\n  /** Cell height in pixels. */\n  height?: number\n  /** Dash pattern for the grid lines. `0` draws them solid. */\n  strokeDasharray?: string | number\n  /** How many cells light up. Set to `0` for a plain static grid. */\n  squares?: number\n  /** Seconds for one full fade in and out of a single cell. */\n  duration?: number\n  /** Changes which cells are chosen. Same seed, same layout, every render. */\n  seed?: number\n  /** Render the grid with no pulsing cells. */\n  disabled?: boolean\n}\n\n/** Mulberry32. Deterministic so the server and the client agree. */\nfunction createRandom(seed: number) {\n  let state = seed >>> 0\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0\n    let t = state\n    t = Math.imul(t ^ (t >>> 15), t | 1)\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n  }\n}\n\nexport function GridBackdrop({\n  width = 40,\n  height = 40,\n  strokeDasharray = 0,\n  squares = 14,\n  duration = 5,\n  seed = 1,\n  disabled = false,\n  className,\n  ...props\n}: GridBackdropProps) {\n  const id = React.useId()\n\n  const cells = React.useMemo(() => {\n    if (disabled || squares <= 0) {\n      return []\n    }\n    const random = createRandom(seed)\n    return Array.from({ length: squares }, (_, index) => ({\n      key: index,\n      x: Math.floor(random() * 30) * width,\n      y: Math.floor(random() * 20) * height,\n      delay: random() * duration * 2,\n    }))\n  }, [disabled, squares, seed, width, height, duration])\n\n  const root = React.useRef<SVGSVGElement>(null)\n  const onScreen = useInViewport(root)\n\n  return (\n    <svg\n      ref={root}\n      data-slot=\"grid-backdrop\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 size-full fill-current/[0.04] stroke-current/[0.08]\",\n        className\n      )}\n      {...props}\n    >\n      <defs>\n        <pattern\n          id={id}\n          width={width}\n          height={height}\n          patternUnits=\"userSpaceOnUse\"\n        >\n          <path\n            d={`M ${width} 0 L 0 0 0 ${height}`}\n            fill=\"none\"\n            strokeDasharray={strokeDasharray}\n          />\n        </pattern>\n      </defs>\n\n      <rect width=\"100%\" height=\"100%\" fill={`url(#${id})`} stroke=\"none\" />\n\n      {/* Opacity is the only thing that moves. The cells never change size or\n          position, so this stays on the compositor. */}\n      <g stroke=\"none\">\n        {cells.map((cell) => (\n          <rect\n            key={cell.key}\n            width={width - 1}\n            height={height - 1}\n            x={cell.x + 1}\n            y={cell.y + 1}\n            className={cn(\n              \"animate-grid-pulse opacity-0 motion-reduce:animate-none\",\n              !onScreen && \"[animation-play-state:paused]\"\n            )}\n            style={{\n              animationDuration: `${duration}s`,\n              animationDelay: `${cell.delay}s`,\n            }}\n          />\n        ))}\n      </g>\n    </svg>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-in-out-cubic": "cubic-bezier(0.645, 0.045, 0.355, 1)",
      "animate-grid-pulse": "grid-pulse 5s var(--ease-in-out-cubic) infinite"
    }
  },
  "css": {
    "@keyframes grid-pulse": {
      "0%, 100%": {
        "opacity": "0"
      },
      "50%": {
        "opacity": "1"
      }
    }
  }
}