{
  "name": "dotted-glow-background",
  "type": "registry:ui",
  "files": [
    {
      "path": "components/ui/dotted-glow-background.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\n\ntype DottedGlowBackgroundProps = {\n  className?: string;\n  /** distance between dot centers in pixels */\n  gap?: number;\n  /** base radius of each dot in CSS px */\n  radius?: number;\n  /** dot color (will pulse by alpha) */\n  color?: string;\n  /** optional dot color for dark mode */\n  darkColor?: string;\n  /** shadow/glow color for bright dots */\n  glowColor?: string;\n  /** optional glow color for dark mode */\n  darkGlowColor?: string;\n  /** optional CSS variable name for light dot color (e.g. --color-zinc-900) */\n  colorLightVar?: string;\n  /** optional CSS variable name for dark dot color (e.g. --color-zinc-100) */\n  colorDarkVar?: string;\n  /** optional CSS variable name for light glow color */\n  glowColorLightVar?: string;\n  /** optional CSS variable name for dark glow color */\n  glowColorDarkVar?: string;\n  /** global opacity for the whole layer */\n  opacity?: number;\n  /** background radial fade opacity (0 = transparent background) */\n  backgroundOpacity?: number;\n  /** minimum per-dot speed in rad/s */\n  speedMin?: number;\n  /** maximum per-dot speed in rad/s */\n  speedMax?: number;\n  /** global speed multiplier for all dots */\n  speedScale?: number;\n};\n\n/**\n * Canvas-based dotted background that randomly glows and dims.\n * - Uses a stable grid of dots.\n * - Each dot gets its own phase + speed producing organic shimmering.\n * - Handles high-DPI and resizes via ResizeObserver.\n */\nexport const DottedGlowBackground = ({\n  className,\n  gap = 12,\n  radius = 2,\n  color = \"rgba(0,0,0,0.7)\",\n  darkColor,\n  glowColor = \"rgba(0, 170, 255, 0.85)\",\n  darkGlowColor,\n  colorLightVar,\n  colorDarkVar,\n  glowColorLightVar,\n  glowColorDarkVar,\n  opacity = 0.6,\n  backgroundOpacity = 0,\n  speedMin = 0.4,\n  speedMax = 1.3,\n  speedScale = 1,\n}: DottedGlowBackgroundProps) => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const [resolvedColor, setResolvedColor] = useState<string>(color);\n  const [resolvedGlowColor, setResolvedGlowColor] = useState<string>(glowColor);\n\n  // Resolve CSS variable value from the container or root\n  const resolveCssVariable = (\n    el: Element,\n    variableName?: string,\n  ): string | null => {\n    if (!variableName) return null;\n    const normalized = variableName.startsWith(\"--\")\n      ? variableName\n      : `--${variableName}`;\n    const fromEl = getComputedStyle(el as Element)\n      .getPropertyValue(normalized)\n      .trim();\n    if (fromEl) return fromEl;\n    const root = document.documentElement;\n    const fromRoot = getComputedStyle(root).getPropertyValue(normalized).trim();\n    return fromRoot || null;\n  };\n\n  const detectDarkMode = (): boolean => {\n    const root = document.documentElement;\n    if (root.classList.contains(\"dark\")) return true;\n    if (root.classList.contains(\"light\")) return false;\n    return (\n      window.matchMedia &&\n      window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n    );\n  };\n\n  // Keep resolved colors in sync with theme changes and prop updates\n  useEffect(() => {\n    const container = containerRef.current ?? document.documentElement;\n\n    const compute = () => {\n      const isDark = detectDarkMode();\n\n      let nextColor: string = color;\n      let nextGlow: string = glowColor;\n\n      if (isDark) {\n        const varDot = resolveCssVariable(container, colorDarkVar);\n        const varGlow = resolveCssVariable(container, glowColorDarkVar);\n        nextColor = varDot || darkColor || nextColor;\n        nextGlow = varGlow || darkGlowColor || nextGlow;\n      } else {\n        const varDot = resolveCssVariable(container, colorLightVar);\n        const varGlow = resolveCssVariable(container, glowColorLightVar);\n        nextColor = varDot || nextColor;\n        nextGlow = varGlow || nextGlow;\n      }\n\n      setResolvedColor(nextColor);\n      setResolvedGlowColor(nextGlow);\n    };\n\n    compute();\n\n    const mql = window.matchMedia\n      ? window.matchMedia(\"(prefers-color-scheme: dark)\")\n      : null;\n    const handleMql = () => compute();\n    mql?.addEventListener?.(\"change\", handleMql);\n\n    const mo = new MutationObserver(() => compute());\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\"],\n    });\n\n    return () => {\n      mql?.removeEventListener?.(\"change\", handleMql);\n      mo.disconnect();\n    };\n  }, [\n    color,\n    darkColor,\n    glowColor,\n    darkGlowColor,\n    colorLightVar,\n    colorDarkVar,\n    glowColorLightVar,\n    glowColorDarkVar,\n  ]);\n\n  useEffect(() => {\n    const el = canvasRef.current;\n    const container = containerRef.current;\n    if (!el || !container) return;\n\n    const ctx = el.getContext(\"2d\");\n    if (!ctx) return;\n\n    let raf = 0;\n    let stopped = false;\n    let isVisible = true;\n\n    const dpr = Math.min(Math.max(1, window.devicePixelRatio || 1), 2);\n\n    const resize = () => {\n      const { width, height } = container.getBoundingClientRect();\n      el.width = Math.max(1, Math.floor(width * dpr));\n      el.height = Math.max(1, Math.floor(height * dpr));\n      el.style.width = `${Math.floor(width)}px`;\n      el.style.height = `${Math.floor(height)}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    // Precompute dot metadata for a medium-sized grid and regenerate on resize\n    let dots: { x: number; y: number; phase: number; speed: number }[] = [];\n\n    const regenDots = () => {\n      dots = [];\n      const { width, height } = container.getBoundingClientRect();\n      const cols = Math.ceil(width / gap) + 2;\n      const rows = Math.ceil(height / gap) + 2;\n      const min = Math.min(speedMin, speedMax);\n      const max = Math.max(speedMin, speedMax);\n      for (let i = -1; i < cols; i++) {\n        for (let j = -1; j < rows; j++) {\n          const x = i * gap + (j % 2 === 0 ? 0 : gap * 0.5); // offset every other row\n          const y = j * gap;\n          // Randomize phase and speed slightly per dot\n          const phase = Math.random() * Math.PI * 2;\n          const span = Math.max(max - min, 0);\n          const speed = min + Math.random() * span; // configurable rad/s\n          dots.push({ x, y, phase, speed });\n        }\n      }\n    };\n\n    const regenThrottled = () => {\n      regenDots();\n    };\n\n    regenDots();\n\n    let last = performance.now();\n\n    const draw = (now: number) => {\n      if (stopped) return;\n      if (!isVisible) {\n        raf = requestAnimationFrame(draw);\n        return;\n      }\n      const dt = (now - last) / 1000; // seconds\n      last = now;\n      const { width, height } = container.getBoundingClientRect();\n\n      ctx.clearRect(0, 0, el.width, el.height);\n      ctx.globalAlpha = opacity;\n\n      // optional subtle background fade for depth (defaults to 0 = transparent)\n      if (backgroundOpacity > 0) {\n        const grad = ctx.createRadialGradient(\n          width * 0.5,\n          height * 0.4,\n          Math.min(width, height) * 0.1,\n          width * 0.5,\n          height * 0.5,\n          Math.max(width, height) * 0.7,\n        );\n        grad.addColorStop(0, \"rgba(0,0,0,0)\");\n        grad.addColorStop(\n          1,\n          `rgba(0,0,0,${Math.min(Math.max(backgroundOpacity, 0), 1)})`,\n        );\n        ctx.fillStyle = grad as unknown as CanvasGradient;\n        ctx.fillRect(0, 0, width, height);\n      }\n\n      // animate dots\n      ctx.save();\n      ctx.fillStyle = resolvedColor;\n\n      const time = (now / 1000) * Math.max(speedScale, 0);\n      for (let i = 0; i < dots.length; i++) {\n        const d = dots[i];\n        // Linear triangle wave 0..1..0 for linear glow/dim\n        const mod = (time * d.speed + d.phase) % 2;\n        const lin = mod < 1 ? mod : 2 - mod; // 0..1..0\n        const a = 0.25 + 0.55 * lin; // 0.25..0.8 linearly\n\n        // draw glow when bright\n        if (a > 0.6) {\n          const glow = (a - 0.6) / 0.4; // 0..1\n          ctx.shadowColor = resolvedGlowColor;\n          ctx.shadowBlur = 6 * glow;\n        } else {\n          ctx.shadowColor = \"transparent\";\n          ctx.shadowBlur = 0;\n        }\n\n        ctx.globalAlpha = a * opacity;\n        ctx.beginPath();\n        ctx.arc(d.x, d.y, radius, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      ctx.restore();\n\n      raf = requestAnimationFrame(draw);\n    };\n\n    const handleResize = () => {\n      resize();\n      regenThrottled();\n    };\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        isVisible = entries[0]?.isIntersecting ?? true;\n      },\n      { threshold: 0.1 },\n    );\n    observer.observe(container);\n\n    window.addEventListener(\"resize\", handleResize);\n    raf = requestAnimationFrame(draw);\n\n    return () => {\n      stopped = true;\n      cancelAnimationFrame(raf);\n      window.removeEventListener(\"resize\", handleResize);\n      observer.disconnect();\n      ro.disconnect();\n    };\n  }, [\n    gap,\n    radius,\n    resolvedColor,\n    resolvedGlowColor,\n    opacity,\n    backgroundOpacity,\n    speedMin,\n    speedMax,\n    speedScale,\n  ]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={className}\n      style={{ position: \"absolute\", inset: 0 }}\n    >\n      <canvas\n        ref={canvasRef}\n        style={{ display: \"block\", width: \"100%\", height: \"100%\" }}\n      />\n    </div>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/dotted-glow-background.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Dotted Glow Background"
}