{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "particles",
  "title": "Particles",
  "description": "Dots drifting slowly across a surface, laid out from a seed and moved with CSS alone.",
  "registryDependencies": [
    "@corsair-ui/utils",
    "@corsair-ui/use-in-view"
  ],
  "files": [
    {
      "path": "registry/default/ui/particles.tsx",
      "content": "\"use client\";\n\nimport { useMemo, type ComponentProps, type CSSProperties, type Ref } from \"react\";\nimport { useInView } from \"@/registry/default/hooks/use-in-view\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    }\n  };\n}\n\nconst MAX_COUNT = 200;\n\n/** A small seeded generator (mulberry32): the same seed gives the same dots on server and client. */\nfunction random(seed: number) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0;\n    let t = state;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst round = (value: number) => Math.round(value * 100) / 100;\n\ninterface ParticlesProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n  /** How many dots, up to 200. */\n  count?: number;\n  /** Which way the dots drift. */\n  direction?: \"up\" | \"down\";\n  /** Average time for one dot to cross, in seconds. */\n  duration?: number;\n  /** Smallest and largest dot, in px. */\n  size?: [number, number];\n  /** Picks where the dots are and how they move; change it for a different scatter. */\n  seed?: number;\n}\n\n/**\n * Dots drifting slowly across a surface, like dust in a light beam or\n * bubbles in water. Each dot is one CSS animation of a transform, laid out\n * from a seed so the server and the browser draw the same scatter. Put it\n * inside a positioned element; colour it with a `text-*` class. It is\n * decorative and hidden from screen readers, pauses off screen, and is not\n * shown at all with `prefers-reduced-motion`.\n *\n * @example\n * <section className=\"relative overflow-hidden rounded-xl p-12\">\n *   <Particles count={40} className=\"text-primary/60\" />\n *   <h2>Below deck</h2>\n * </section>\n */\nfunction Particles({\n  count = 30,\n  direction = \"up\",\n  duration = 14,\n  size = [1, 3],\n  seed = 1,\n  className,\n  ref,\n  ...props\n}: ParticlesProps) {\n  const [observe, inView] = useInView<HTMLDivElement>({ rootMargin: \"100px\" });\n  const mergedRef = useMemo(() => mergeRefs(ref, observe), [ref, observe]);\n  const [smallest, largest] = size;\n\n  const dots = useMemo(() => {\n    const next = random(seed);\n    const total = Math.min(Math.max(Math.floor(count), 0), MAX_COUNT);\n    const low = Math.max(Math.min(smallest, largest), 0);\n    const high = Math.max(smallest, largest, low);\n    return Array.from({ length: total }, () => {\n      const trip = Math.max(duration, 0.1) * (0.6 + next() * 0.8);\n      return {\n        left: round(next() * 100),\n        size: round(low + next() * (high - low)),\n        duration: round(trip),\n        // Negative, so every dot is already partway across on the first frame.\n        delay: round(-next() * trip),\n        drift: round((next() - 0.5) * 60),\n        opacity: round(0.3 + next() * 0.7),\n      };\n    });\n  }, [count, duration, smallest, largest, seed]);\n\n  return (\n    <div\n      ref={mergedRef}\n      aria-hidden=\"true\"\n      data-slot=\"particles\"\n      data-direction={direction}\n      className={cn(\n        \"text-muted-foreground pointer-events-none absolute inset-0 overflow-hidden motion-reduce:hidden\",\n        className\n      )}\n      {...props}\n    >\n      {dots.map((dot, index) => (\n        // Each track is as tall as the surface, so moving it by its own\n        // height carries the dot from one edge to the other.\n        <span\n          key={index}\n          data-slot=\"particles-dot\"\n          className={cn(\n            \"absolute inset-y-0\",\n            direction === \"up\"\n              ? \"motion-safe:animate-particles-up\"\n              : \"motion-safe:animate-particles-down\"\n          )}\n          style={\n            {\n              left: `${dot.left}%`,\n              width: `${dot.size}px`,\n              \"--particles-drift\": `${dot.drift}px`,\n              \"--particles-opacity\": dot.opacity,\n              animationDuration: `${dot.duration}s`,\n              animationDelay: `${dot.delay}s`,\n              animationPlayState: inView ? undefined : \"paused\",\n            } as CSSProperties\n          }\n        >\n          <span\n            className={cn(\n              \"absolute left-0 rounded-full bg-current\",\n              direction === \"up\" ? \"bottom-0\" : \"top-0\"\n            )}\n            style={{ width: `${dot.size}px`, height: `${dot.size}px` }}\n          />\n        </span>\n      ))}\n    </div>\n  );\n}\n\nexport { Particles, type ParticlesProps };\n",
      "type": "registry:ui"
    }
  ],
  "tailwind": {
    "config": {
      "theme": {
        "extend": {
          "keyframes": {
            "particles-up": {
              "0%": {
                "transform": "translate3d(0, 0, 0)",
                "opacity": "0"
              },
              "15%, 85%": {
                "opacity": "var(--particles-opacity, 1)"
              },
              "100%": {
                "transform": "translate3d(var(--particles-drift, 0), -100%, 0)",
                "opacity": "0"
              }
            },
            "particles-down": {
              "0%": {
                "transform": "translate3d(0, 0, 0)",
                "opacity": "0"
              },
              "15%, 85%": {
                "opacity": "var(--particles-opacity, 1)"
              },
              "100%": {
                "transform": "translate3d(var(--particles-drift, 0), 100%, 0)",
                "opacity": "0"
              }
            }
          },
          "animation": {
            "particles-up": "particles-up 14s linear infinite",
            "particles-down": "particles-down 14s linear infinite"
          }
        }
      }
    }
  },
  "cssVars": {
    "theme": {
      "animate-particles-up": "particles-up 14s linear infinite",
      "animate-particles-down": "particles-down 14s linear infinite"
    }
  },
  "css": {
    "@keyframes particles-up": {
      "0%": {
        "transform": "translate3d(0, 0, 0)",
        "opacity": "0"
      },
      "15%, 85%": {
        "opacity": "var(--particles-opacity, 1)"
      },
      "100%": {
        "transform": "translate3d(var(--particles-drift, 0), -100%, 0)",
        "opacity": "0"
      }
    },
    "@keyframes particles-down": {
      "0%": {
        "transform": "translate3d(0, 0, 0)",
        "opacity": "0"
      },
      "15%, 85%": {
        "opacity": "var(--particles-opacity, 1)"
      },
      "100%": {
        "transform": "translate3d(var(--particles-drift, 0), 100%, 0)",
        "opacity": "0"
      }
    }
  },
  "categories": [
    "motion",
    "background"
  ],
  "type": "registry:ui"
}