{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-grid-pattern",
  "type": "registry:ui",
  "title": "Animated Grid Pattern",
  "description": "A tiled SVG grid where a handful of cells softly pulse in and out, then teleport to a new random cell on each cycle — a \"breathing\" graph-paper backdrop for heroes and section bands. Re-authored from the MagicUI pattern to the Interlace component floor.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "decorative",
    "effect"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/magicui/animated-grid-pattern.tsx",
      "target": "components/ui/magicui/animated-grid-pattern.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n} from \"react\";\n\n// @interlace/animated-grid-pattern v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/animated-grid-pattern\n// What changed since: https://ds.interlace.tools/c/animated-grid-pattern#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — AnimatedGridPattern (decorative background primitive)\n *\n * A tiled SVG grid where a handful of cells softly pulse in and out, then\n * teleport to a new random cell on each cycle — a \"breathing\" graph-paper\n * backdrop for heroes and section bands. Re-authored from the MagicUI\n * pattern to the Interlace component floor.\n *\n * | Rule | Concept                          | Where in this file                                                                 |\n * | ---- | -------------------------------- | ---------------------------------------------------------------------------------- |\n * | R4   | Extends native el + JSDoc        | `ComponentPropsWithoutRef<\"svg\">`; every public prop documented with `@default`    |\n * | R5   | `data-testid` typed, no default  | `\"data-testid\"?: string` — consumer supplies the selector                          |\n * | R6   | `data-slot` on the root          | `data-slot=\"animated-grid-pattern\"` on the `<svg>`                                 |\n * | R7   | className merged + ...rest + ref | `cn(...)`, `{...props}`, `ref` forwarded to the root `<svg>`                        |\n * | R8   | Booleans no `isXxx`              | No boolean props; numeric/string knobs only                                        |\n * | R18  | Tailwind only                    | No inline `style`; geometry is SVG attributes, color is a Tailwind class           |\n * | R19  | Tokens only — no raw color       | Color via `currentColor` (default class `text-muted-foreground/20`); no hex     |\n * | R23  | CLS=0                            | `absolute inset-0 pointer-events-none` decorative chrome — reserves no flow space  |\n * | R25  | Perf — `'use client'` justified  | Reads `ResizeObserver` + `useReducedMotion`; gated to interactive use only         |\n * | R26  | A11y                            | `aria-hidden` — purely decorative, never announced; zero axe suppressions          |\n *\n * ## API parity\n *\n * Mirrors the MagicUI `AnimatedGridPattern` prop surface (width / height /\n * x / y / strokeDasharray / numSquares / maxOpacity / duration /\n * repeatDelay) so the swap is drop-in. Deviations from upstream, each with\n * a reason:\n *   - Upstream hard-codes `fill-gray-400/30 stroke-gray-400/30` and\n *     `fill=\"currentColor\"`. We drop the literal gray classes and inherit\n *     `currentColor` from a tokenized default class — color is now themeable\n *     by the consumer via `text-*` utilities (R19).\n *   - Upstream has no reduced-motion path; we read `useReducedMotion()` and\n *     render a static grid (no pulsing cells) when the user opts out (R26).\n *   - `numSquares` is clamped to the number of grid cells that actually fit,\n *     so a small container can't request 50 squares into a 12-cell grid and\n *     stack invisible duplicates.\n */\n\nimport { motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\n\ninterface AnimatedGridPatternProps extends ComponentPropsWithoutRef<\"svg\"> {\n  /**\n   * Width of a single grid cell, in pixels.\n   * @default 40\n   */\n  width?: number;\n  /**\n   * Height of a single grid cell, in pixels.\n   * @default 40\n   */\n  height?: number;\n  /**\n   * Horizontal offset of the pattern origin, in pixels. A small negative\n   * value hides the seam at the left edge.\n   * @default -1\n   */\n  x?: number;\n  /**\n   * Vertical offset of the pattern origin, in pixels. A small negative\n   * value hides the seam at the top edge.\n   * @default -1\n   */\n  y?: number;\n  /**\n   * Dash length for the grid lines. `0` draws solid lines.\n   * @default 0\n   */\n  strokeDasharray?: number;\n  /**\n   * Number of cells pulsing at once. Clamped to the cells that fit the\n   * measured container so a small surface never requests more than it has.\n   * @default 50\n   */\n  numSquares?: number;\n  /**\n   * Peak opacity each cell fades to at the top of its pulse (0–1).\n   * @default 0.5\n   */\n  maxOpacity?: number;\n  /**\n   * Duration of a single fade-in/out cycle, in seconds.\n   * @default 4\n   */\n  duration?: number;\n  /**\n   * Pause between a cell finishing its cycle and teleporting to a new\n   * position, in seconds.\n   * @default 0.5\n   */\n  repeatDelay?: number;\n  /**\n   * Stable selector for E2E tests. Consumer supplies it — no runtime\n   * default, so an omission is visible in test failures rather than masked.\n   */\n  \"data-testid\"?: string;\n}\n\ninterface CellMeta {\n  id: number;\n  pos: [number, number];\n  iteration: number;\n}\n\nconst MIN_CELLS = 1;\n\n/**\n * AnimatedGridPattern — a tiled SVG grid with softly pulsing cells.\n *\n * Decorative by contract: `aria-hidden` and `pointer-events-none`, absolutely\n * positioned to fill its nearest positioned ancestor. Respects\n * `prefers-reduced-motion` by rendering a static grid with no pulsing cells.\n *\n * @example\n * ```tsx\n * <div className=\"relative overflow-hidden\">\n *   <AnimatedGridPattern\n *     numSquares={30}\n *     maxOpacity={0.4}\n *     className=\"text-primary/20 [mask-image:radial-gradient(400px_circle_at_center,white,transparent)]\"\n *   />\n * </div>\n * ```\n */\nconst AnimatedGridPattern = forwardRef<SVGSVGElement, AnimatedGridPatternProps>(\n  function AnimatedGridPattern(\n    {\n      width = 40,\n      height = 40,\n      x = -1,\n      y = -1,\n      strokeDasharray = 0,\n      numSquares = 50,\n      maxOpacity = 0.5,\n      duration = 4,\n      repeatDelay = 0.5,\n      className,\n      \"data-testid\": testId,\n      ...props\n    },\n    ref,\n  ) {\n    const patternId = useId();\n    const containerRef = useRef<SVGSVGElement | null>(null);\n    const reducedMotion = useReducedMotion();\n    const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n    const [cells, setCells] = useState<CellMeta[]>([]);\n\n    // Clamp the requested count to the cells that actually fit; an\n    // unmeasured container yields 0, which renders nothing until measured.\n    const cellCount = useMemo(() => {\n      if (!dimensions.width || !dimensions.height) return 0;\n      const cols = Math.floor(dimensions.width / width);\n      const rows = Math.floor(dimensions.height / height);\n      const capacity = Math.max(MIN_CELLS, cols * rows);\n      return Math.min(numSquares, capacity);\n    }, [dimensions.width, dimensions.height, width, height, numSquares]);\n\n    const getPos = useCallback(\n      (): [number, number] => [\n        Math.floor((Math.random() * dimensions.width) / width),\n        Math.floor((Math.random() * dimensions.height) / height),\n      ],\n      [dimensions.height, dimensions.width, height, width],\n    );\n\n    const generateCells = useCallback(\n      (count: number): CellMeta[] =>\n        Array.from({ length: count }, (_, i) => ({\n          id: i,\n          pos: getPos(),\n          iteration: 0,\n        })),\n      [getPos],\n    );\n\n    // Teleport a single cell to a fresh position once its pulse completes.\n    const recycleCell = useCallback(\n      (cellId: number) => {\n        setCells((current) => {\n          const cell = current[cellId];\n          if (!cell || cell.id !== cellId) return current;\n          const next = current.slice();\n          next[cellId] = {\n            ...cell,\n            pos: getPos(),\n            iteration: cell.iteration + 1,\n          };\n          return next;\n        });\n      },\n      [getPos],\n    );\n\n    // (Re)seed the pulsing cells whenever the measured size or count changes.\n    useEffect(() => {\n      if (cellCount > 0) {\n        setCells(generateCells(cellCount));\n      } else {\n        setCells([]);\n      }\n    }, [cellCount, generateCells]);\n\n    // Measure the container so cell positions land on real grid coordinates.\n    useEffect(() => {\n      const element = containerRef.current;\n      if (!element || typeof ResizeObserver === \"undefined\") return;\n\n      const observer = new ResizeObserver((entries) => {\n        for (const entry of entries) {\n          setDimensions((current) => {\n            const nextWidth = entry.contentRect.width;\n            const nextHeight = entry.contentRect.height;\n            if (current.width === nextWidth && current.height === nextHeight) {\n              return current;\n            }\n            return { width: nextWidth, height: nextHeight };\n          });\n        }\n      });\n\n      observer.observe(element);\n      return () => observer.disconnect();\n    }, []);\n\n    const setRefs = useCallback(\n      (node: SVGSVGElement | null) => {\n        containerRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      },\n      [ref],\n    );\n\n    return (\n      <svg\n        ref={setRefs}\n        data-slot=\"animated-grid-pattern\"\n        data-testid={testId}\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-0 h-full w-full fill-current stroke-current text-muted-foreground/20\",\n          className,\n        )}\n        {...props}\n      >\n        <defs>\n          <pattern\n            id={patternId}\n            width={width}\n            height={height}\n            patternUnits=\"userSpaceOnUse\"\n            x={x}\n            y={y}\n          >\n            <path\n              d={`M.5 ${height}V.5H${width}`}\n              fill=\"none\"\n              strokeDasharray={strokeDasharray}\n            />\n          </pattern>\n        </defs>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${patternId})`} />\n        <svg x={x} y={y} className=\"overflow-visible\">\n          {cells.map(({ pos: [cellX, cellY], id, iteration }, index) =>\n            reducedMotion ? (\n              // Reduced motion: a static accent cell, no pulsing.\n              <rect\n                key={`${id}-static`}\n                width={width - 1}\n                height={height - 1}\n                x={cellX * width + 1}\n                y={cellY * height + 1}\n                fill=\"currentColor\"\n                strokeWidth=\"0\"\n                opacity={maxOpacity}\n              />\n            ) : (\n              <motion.rect\n                key={`${id}-${iteration}`}\n                initial={{ opacity: 0 }}\n                animate={{ opacity: maxOpacity }}\n                transition={{\n                  duration,\n                  repeat: 1,\n                  delay: index * 0.1,\n                  repeatType: \"reverse\",\n                  repeatDelay,\n                }}\n                onAnimationComplete={() => recycleCell(id)}\n                width={width - 1}\n                height={height - 1}\n                x={cellX * width + 1}\n                y={cellY * height + 1}\n                fill=\"currentColor\"\n                strokeWidth=\"0\"\n              />\n            ),\n          )}\n        </svg>\n      </svg>\n    );\n  },\n);\n\nexport { AnimatedGridPattern };\nexport type { AnimatedGridPatternProps };\n"
    }
  ],
  "meta": {
    "tier": "effect",
    "client": true,
    "minViewport": null,
    "loading": false,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/animated-grid-pattern\n\nInstalled to `components/ui/magicui/animated-grid-pattern.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/magicui/animated-grid-pattern';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/animated-grid-pattern\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
