{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-pattern",
  "title": "Dot Pattern",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A lightweight SVG dot grid background with an optional radial fade and a few softly pulsing glow dots. One pattern element, token colors, hydration-safe, still under reduced motion.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/dot-pattern.tsx",
      "content": "// Ballmac UI: Dot Pattern. https://ui.ballmac.com/components/dot-pattern\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype DotPatternProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Horizontal spacing between dots in pixels. */\n  width?: number\n  /** Vertical spacing between dots in pixels. */\n  height?: number\n  /** Dot radius in pixels. */\n  radius?: number\n  /** Make a few random dots glow and pulse softly. */\n  glow?: boolean\n  /** How many dots glow at once when `glow` is on. */\n  glowCount?: number\n  /** Color of the glowing dots. Any CSS color or var(). */\n  glowColor?: string\n  /** Fade the pattern out toward the edges with a radial mask. */\n  radialMask?: boolean\n}\n\ntype GlowDot = { id: number; x: number; y: number; delay: number; duration: number }\n\nfunction DotPattern({\n  width = 16,\n  height = 16,\n  radius = 1,\n  glow = false,\n  glowCount = 18,\n  glowColor = \"var(--chart-1)\",\n  radialMask = true,\n  className,\n  style,\n  ...props\n}: DotPatternProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const patternId = `bm-dots-${React.useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`\n  // Glow positions are random, so they're picked on the client after mount (hydration-safe).\n  const [dots, setDots] = React.useState<GlowDot[]>([])\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || !glow) {\n      setDots([])\n      return\n    }\n    const pick = () => {\n      const cols = Math.floor(root.clientWidth / width)\n      const rows = Math.floor(root.clientHeight / height)\n      if (cols < 1 || rows < 1) return\n      setDots(\n        Array.from({ length: glowCount }, (_, id) => ({\n          id,\n          x: (Math.floor(Math.random() * cols) + 0.5) * width,\n          y: (Math.floor(Math.random() * rows) + 0.5) * height,\n          delay: Math.random() * 4,\n          duration: 2.5 + Math.random() * 3,\n        }))\n      )\n    }\n    pick()\n    const ro = new ResizeObserver(pick)\n    ro.observe(root)\n    return () => ro.disconnect()\n  }, [glow, glowCount, width, height])\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || !dots.length || typeof root.animate !== \"function\") return\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return\n    const animations = Array.from(root.querySelectorAll<SVGCircleElement>(\"[data-slot=dot-pattern-glow]\")).map(\n      (node, i) =>\n        node.animate([{ opacity: 0.15 }, { opacity: 1 }, { opacity: 0.15 }], {\n          duration: dots[i]!.duration * 1000,\n          delay: -dots[i]!.delay * 1000,\n          iterations: Infinity,\n          easing: \"ease-in-out\",\n        })\n    )\n    let visible = true\n    const sync = () => {\n      for (const a of animations) {\n        if (visible && !document.hidden) a.play()\n        else a.pause()\n      }\n    }\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? true\n      sync()\n    })\n    io.observe(root)\n    document.addEventListener(\"visibilitychange\", sync)\n    return () => {\n      io.disconnect()\n      document.removeEventListener(\"visibilitychange\", sync)\n      for (const a of animations) a.cancel()\n    }\n  }, [dots])\n\n  const mask = radialMask ? \"radial-gradient(ellipse 70% 65% at 50% 50%, black 25%, transparent 80%)\" : undefined\n\n  return (\n    <div\n      ref={rootRef}\n      aria-hidden=\"true\"\n      data-slot=\"dot-pattern\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 overflow-hidden [--dot:color-mix(in_oklch,var(--foreground)_30%,transparent)]\",\n        className\n      )}\n      style={{ maskImage: mask, WebkitMaskImage: mask, ...style }}\n      {...props}\n    >\n      <svg className=\"absolute inset-0 size-full\">\n        <defs>\n          <pattern id={patternId} width={width} height={height} patternUnits=\"userSpaceOnUse\">\n            <circle cx={width / 2} cy={height / 2} r={radius} style={{ fill: \"var(--dot)\" }} />\n          </pattern>\n        </defs>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${patternId})`} />\n        {dots.map((dot) => (\n            <circle\n              key={dot.id}\n              data-slot=\"dot-pattern-glow\"\n              cx={dot.x}\n              cy={dot.y}\n              r={radius * 1.8}\n              style={{\n                fill: glowColor,\n                opacity: 0.6,\n                filter: `drop-shadow(0 0 ${radius * 4}px ${glowColor})`,\n              }}\n            />\n        ))}\n      </svg>\n    </div>\n  )\n}\n\nexport { DotPattern, type DotPatternProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/dot-pattern.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "dots",
      "pattern",
      "grid",
      "svg",
      "background",
      "decorative"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Place <DotPattern /> as the first child of a relative, overflow-hidden container (absolute inset-0, aria-hidden, pointer-events-none). Add glow for a few pulsing accent dots. The cheapest Ballmac background; safe for large areas.",
      "whenToUse": [
        "Quiet texture behind heroes, feature cards, empty states and auth pages",
        "Large page areas where canvas effects would be too heavy"
      ],
      "whenNotToUse": [
        "When you want visible motion as the focal point (use flickering-grid or beams-background)"
      ],
      "composesWith": [
        "orbiting-circles",
        "animated-beam",
        "spotlight-card"
      ],
      "a11y": [],
      "customization": [
        "width / height: dot spacing in px (default 16); radius (default 1)",
        "glow, glowCount (default 18), glowColor (default var(--chart-1))",
        "radialMask toggles the edge fade; dot color is the --dot CSS variable: className=\"[--dot:var(--chart-2)]\""
      ]
    },
    "examples": [
      "dot-pattern-demo",
      "dot-pattern-dense"
    ],
    "url": "https://ui.ballmac.com/components/dot-pattern"
  },
  "categories": [
    "backgrounds"
  ],
  "type": "registry:ui"
}