{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkling-grid",
  "type": "registry:ui",
  "title": "Sparkling Grid",
  "description": "The SparklingGrid component creates an animated grid of dots that ripples outward on mount and sparkles at random, ideal for dynamic, atmospheric backgrounds.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/demo/background/sparkling-grid.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\ninterface SparklingGridProps {\n  /** Spacing between dots, in pixels. */\n  gridSize?: number;\n  /** Chance (0–1) that a given dot sparkles on each of its ticks. */\n  sparkleFrequency?: number;\n  className?: string;\n}\n\n/**\n * An animated grid of dots that ripples outward on mount, then sparkles at\n * random.\n *\n * Colour comes from `currentColor`, so it inherits whatever text colour the\n * parent sets and follows the theme with no JS involved.\n */\nexport function SparklingGrid({\n  gridSize = 30,\n  sparkleFrequency = 0.03,\n  className = \"\",\n}: SparklingGridProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const prefersReducedMotion = useReducedMotion();\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    // Every timer is tracked so unmount can cancel all of them. The previous\n    // version left the sparkle loop rescheduling itself forever after the\n    // component was gone.\n    const timers = new Set<ReturnType<typeof setTimeout>>();\n    const later = (fn: () => void, ms: number) => {\n      const id = setTimeout(() => {\n        timers.delete(id);\n        fn();\n      }, ms);\n      timers.add(id);\n      return id;\n    };\n\n    const build = () => {\n      container.replaceChildren();\n\n      const { offsetWidth: width, offsetHeight: height } = container;\n      if (width === 0 || height === 0) return;\n\n      const rows = Math.ceil(height / gridSize);\n      const cols = Math.ceil(width / gridSize);\n      const centerRow = rows / 2;\n      const centerCol = cols / 2;\n      const maxDistance = Math.hypot(centerRow, centerCol) || 1;\n\n      const fragment = document.createDocumentFragment();\n\n      for (let row = 0; row < rows; row++) {\n        for (let col = 0; col < cols; col++) {\n          const dot = document.createElement(\"span\");\n          dot.className =\n            \"absolute size-[3px] rounded-full bg-current transition-all duration-700\";\n          dot.style.left = `${col * gridSize}px`;\n          dot.style.top = `${row * gridSize}px`;\n\n          if (prefersReducedMotion) {\n            dot.style.opacity = \"0.2\";\n            fragment.appendChild(dot);\n            continue;\n          }\n\n          dot.style.opacity = \"0\";\n          dot.style.transform = \"scale(0)\";\n          fragment.appendChild(dot);\n\n          // Distance from the centre drives the delay, which is what makes\n          // the grid appear as an outward ripple.\n          const delay =\n            (Math.hypot(col - centerCol, row - centerRow) / maxDistance) * 1200;\n\n          later(() => {\n            dot.style.opacity = \"0.2\";\n            dot.style.transform = \"scale(1)\";\n          }, delay);\n\n          const sparkle = () => {\n            if (Math.random() < sparkleFrequency) {\n              dot.style.opacity = \"1\";\n              dot.style.boxShadow = \"0 0 6px currentColor\";\n              later(() => {\n                dot.style.opacity = \"0.2\";\n                dot.style.boxShadow = \"\";\n              }, 400);\n            }\n            later(sparkle, 1000 + Math.random() * 4000);\n          };\n\n          later(sparkle, delay + 1000);\n        }\n      }\n\n      container.appendChild(fragment);\n    };\n\n    build();\n\n    // Rebuild when the container resizes, so the grid fills a responsive\n    // parent instead of keeping its first-paint dimensions.\n    const observer = new ResizeObserver(() => build());\n    observer.observe(container);\n\n    return () => {\n      observer.disconnect();\n      for (const id of timers) clearTimeout(id);\n      timers.clear();\n      container.replaceChildren();\n    };\n    // Primitive deps only. The old effect listed the `sparkleColor` and\n    // `dotColor` object props, which are fresh literals on every render when\n    // defaulted — so it tore down and rebuilt the entire grid continuously.\n  }, [gridSize, sparkleFrequency, prefersReducedMotion]);\n\n  return (\n    <motion.div\n      ref={containerRef}\n      aria-hidden=\"true\"\n      // No negative z-index. `-z-50` put the dots behind the parent's own\n      // background, which is why the grid rendered as nothing inside the docs\n      // preview. Consumers layer their content with `relative z-10` instead.\n      className={`pointer-events-none absolute inset-0 overflow-hidden text-foreground ${className}`}\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1 }}\n      transition={{ duration: 1.2 }}\n    />\n  );\n}\n\nexport default SparklingGrid;\n",
      "type": "registry:ui",
      "target": "components/ui/sparkling-grid.tsx"
    }
  ]
}
