{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-grid","type":"registry:ui","title":"Motion Grid","description":"A grid of tiles that shrink, spin and flash in rolling waves radiating from whichever tile you click.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/motion-grid.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MotionGridProps {\n  rows?: number;\n  cols?: number;\n  /** Px between tiles. */\n  gap?: number;\n  /** Ms of delay per tile of distance from the origin. */\n  stagger?: number;\n  /** Ms each tile takes to complete its wave. */\n  duration?: number;\n  /** Color tiles flash as the wave passes. */\n  accent?: string;\n  /** Launch waves from random tiles on its own every few seconds. */\n  autoplay?: boolean;\n  className?: string;\n}\n\nexport function MotionGrid({\n  rows = 9,\n  cols = 15,\n  gap = 6,\n  stagger = 45,\n  duration = 900,\n  accent = \"#c6ff3d\",\n  autoplay = true,\n  className,\n}: MotionGridProps) {\n  const root = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const el = root.current!;\n    const tiles = Array.from(el.children) as HTMLElement[];\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let timer = 0;\n    // Read the resting tile color once, before any wave can tint it.\n    const base = tiles.length ? getComputedStyle(tiles[0]).backgroundColor : \"transparent\";\n\n    const wave = (r0: number, c0: number) => {\n      if (reduced) return;\n      tiles.forEach((tile, i) => {\n        const r = Math.floor(i / cols);\n        const c = i % cols;\n        const d = Math.hypot(r - r0, c - c0);\n        const spin = (r + c) % 2 ? 90 : -90;\n        tile.animate(\n          [\n            { transform: \"scale(1) rotate(0deg)\", background: base, opacity: 1 },\n            { transform: `scale(0.4) rotate(${spin}deg)`, background: accent, opacity: 0.7, offset: 0.3 },\n            { transform: `scale(1.12) rotate(${spin}deg)`, background: accent, opacity: 1, offset: 0.62 },\n            { transform: `scale(1) rotate(${spin}deg)`, background: base },\n          ],\n          { duration, delay: d * stagger, easing: \"cubic-bezier(.4,0,.2,1)\" },\n        );\n      });\n    };\n\n    const onClick = (e: MouseEvent) => {\n      const i = tiles.indexOf(e.target as HTMLElement);\n      if (i >= 0) wave(Math.floor(i / cols), i % cols);\n    };\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"Enter\" && e.key !== \" \") return;\n      e.preventDefault();\n      wave((rows - 1) / 2, (cols - 1) / 2);\n    };\n    el.addEventListener(\"click\", onClick);\n    el.addEventListener(\"keydown\", onKey);\n\n    if (autoplay && !reduced) {\n      const auto = () => {\n        if (document.visibilityState === \"visible\") wave(Math.floor(Math.random() * rows), Math.floor(Math.random() * cols));\n        timer = window.setTimeout(auto, 3200 + Math.hypot(rows, cols) * stagger);\n      };\n      timer = window.setTimeout(auto, 600);\n    }\n    return () => {\n      clearTimeout(timer);\n      el.removeEventListener(\"click\", onClick);\n      el.removeEventListener(\"keydown\", onKey);\n      tiles.forEach((t) => t.getAnimations().forEach((a) => a.cancel()));\n    };\n  }, [rows, cols, stagger, duration, accent, autoplay]);\n\n  return (\n    <div\n      ref={root}\n      tabIndex={0}\n      role=\"button\"\n      aria-label=\"Motion grid: click a tile or press Enter to send a wave\"\n      className={cn(\"grid cursor-pointer rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring\", className)}\n      style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gap }}\n    >\n      {Array.from({ length: rows * cols }, (_, i) => (\n        <div key={i} className=\"aspect-square rounded-[22%] bg-muted\" style={{ willChange: \"transform\" }} />\n      ))}\n    </div>\n  );\n}\n"}]}