{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "background-beams-with-collision",
  "type": "registry:ui",
  "title": "Background Beams With Collision",
  "description": "A hero surface where thin vertical beams fall from the top and burst into a ten-particle explosion when they reach the strip along the bottom. Your children render above them on a `z-10` layer.",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "decorative",
    "effect"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/aceternity/background-beams-with-collision.tsx",
      "target": "components/ui/aceternity/background-beams-with-collision.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\n// @interlace/background-beams-with-collision v1.2.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/background-beams-with-collision\n// What changed since: https://ds.interlace.tools/c/background-beams-with-collision#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — BackgroundBeamsWithCollision\n *\n * A hero surface where thin vertical beams fall from the top and burst into a\n * ten-particle explosion when they reach the strip along the bottom. Your\n * children render above them on a `z-10` layer.\n *\n * The container is `h-96 md:h-[40rem]` unless `containerClassName` says\n * otherwise.\n *\n * Our reimplementation of the Aceternity UI effect of the same name. What is\n * ours: the beams are painted from `--primary` / `--chart-2` rather than the\n * upstream purple/indigo literals, so they re-resolve per theme; an\n * `IntersectionObserver` unmounts the whole beam layer when the hero scrolls\n * out of view; the collision poll runs at 200ms instead of 50ms; and the\n * explosion is ten particles instead of twenty.\n *\n * ## Anatomy\n *\n *   div                              (parent — gradient, overflow-hidden,\n *                                     contain: layout style paint)\n *     ├─ CollisionMechanism ×beams   (motion.div beam + AnimatePresence)\n *     │   └─ Explosion               (glow line + 10 motion.span particles)\n *     ├─ div.z-10                    (your children)\n *     └─ div                         (collision surface, bottom strip —\n *                                     styling suppressed by hideCollisionSurface)\n *\n * Each beam is a `BeamConfig` (`initialX`, `translateY`, `duration`, `delay`,\n * `repeatDelay`, `className`); `beams` replaces the seven-beam default wholesale.\n *\n * ## Motion\n *\n * JS-driven throughout — `motion/react` transforms for the fall, a\n * `setInterval` collision poll, and `AnimatePresence` for the burst. None of\n * it is reachable by the CSS reset in `styles/preflight.css`, so the gate is\n * in JS and it is total: `{isVisible && !reduceMotion && beams.map(…)}` means\n * that under `prefers-reduced-motion: reduce` no `CollisionMechanism` mounts\n * at all — no beams, no interval, no explosions. What remains is the gradient\n * background, the collision strip, and your content. That is the intended\n * still state, not a degraded one.\n *\n * ## Where it leaves the token system\n *\n * The beams and the explosion are tokenised, but the surface around them is\n * not: the parent gradient is `from-white to-neutral-100` /\n * `dark:from-neutral-950 … dark:to-neutral-900`, the collision strip is\n * `bg-neutral-100` / `dark:bg-neutral-900/50`, and its `boxShadow` is a stack\n * of raw `rgba()` literals. Pass `containerClassName` to put the surface back\n * on your own background.\n *\n * The explosion's particle directions come from `Math.random()`, so no two\n * bursts match and the effect is not snapshot-testable.\n */\n\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport React, { useRef, useState, useEffect } from \"react\";\n\n// =========================================\n// TYPES\n// =========================================\n\nexport interface BeamConfig {\n  initialX?: number;\n  translateX?: number;\n  initialY?: number;\n  translateY?: number;\n  rotate?: number;\n  className?: string;\n  duration?: number;\n  delay?: number;\n  repeatDelay?: number;\n}\n\nexport interface BackgroundBeamsWithCollisionProps {\n  children: React.ReactNode;\n  className?: string;\n  containerClassName?: string;\n  beams?: BeamConfig[];\n  /** When true, hides the visible collision surface at the bottom (useful for full-page backgrounds) */\n  hideCollisionSurface?: boolean;\n}\n\ninterface CollisionState {\n  detected: boolean;\n  coordinates: { x: number; y: number } | null;\n}\n\n// =========================================\n// CONFIGURATION - Beam Definitions\n// =========================================\n\n/**\n * Default beam configuration - staggered across the viewport\n * Each beam has unique timing and position for visual variety\n */\nconst DEFAULT_BEAMS: BeamConfig[] = [\n  { initialX: 10, translateX: 10, duration: 7, repeatDelay: 3, delay: 2 },\n  { initialX: 600, translateX: 600, duration: 3, repeatDelay: 3, delay: 4 },\n  { initialX: 100, translateX: 100, duration: 7, repeatDelay: 7, className: \"h-6\" },\n  { initialX: 400, translateX: 400, duration: 5, repeatDelay: 14, delay: 4 },\n  { initialX: 800, translateX: 800, duration: 11, repeatDelay: 2, className: \"h-20\" },\n  { initialX: 1000, translateX: 1000, duration: 4, repeatDelay: 2, className: \"h-12\" },\n  { initialX: 1200, translateX: 1200, duration: 6, repeatDelay: 4, delay: 2, className: \"h-6\" },\n];\n\n// =========================================\n// MAIN COMPONENT\n// =========================================\n\nexport const BackgroundBeamsWithCollision = ({\n  children,\n  className,\n  containerClassName,\n  beams = DEFAULT_BEAMS,\n  hideCollisionSurface = false,\n}: BackgroundBeamsWithCollisionProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const parentRef = useRef<HTMLDivElement>(null);\n  const [isVisible, setIsVisible] = useState(true);\n  const reduceMotion = useReducedMotion();\n\n  // Pause animations when component is not visible (performance optimization)\n  useEffect(() => {\n    if (!parentRef.current) return;\n    \n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        setIsVisible(entry.isIntersecting);\n      },\n      { threshold: 0.1 }\n    );\n    \n    observer.observe(parentRef.current);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <div\n      ref={parentRef}\n      className={cn(\n        // Base layout\n        \"relative flex w-full items-center justify-center overflow-hidden\",\n        // Default height (can be overridden via className)\n        \"h-96 md:h-[40rem]\",\n        // Theme-aware background gradient\n        // Light mode: Clean white to neutral gradient\n        \"bg-gradient-to-b from-white to-neutral-100\",\n        // Dark mode: a warm brand wash, not the inherited purple\n        \"dark:from-neutral-950 dark:via-primary/8 dark:to-neutral-900\",\n        containerClassName\n      )}\n      // Performance: CSS containment to reduce layout thrashing\n      style={{ contain: 'layout style paint' }}\n    >\n      {/* Render collision beams — gated on visibility AND reduced-motion. */}\n      {isVisible && !reduceMotion && beams.map((beam) => (\n        <CollisionMechanism\n          key={`${beam.initialX}-beam`}\n          beamOptions={beam}\n          containerRef={containerRef}\n          parentRef={parentRef}\n        />\n      ))}\n\n      {/* Content wrapper */}\n      <div className={cn(\"relative z-10\", className)}>{children}</div>\n\n      {/* Collision surface at bottom */}\n      <div\n        ref={containerRef}\n        className={cn(\n          \"absolute bottom-0 inset-x-0 w-full pointer-events-none\",\n          // Only show visual styling when not hidden\n          !hideCollisionSurface && [\n            // Light mode: Subtle neutral with soft shadow\n            \"bg-neutral-100\",\n            // Dark mode: Darker surface with purple tint\n            \"dark:bg-neutral-900/50\"\n          ]\n        )}\n        style={hideCollisionSurface ? undefined : {\n          boxShadow:\n            \"0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset\",\n        }}\n      />\n    </div>\n  );\n};\n\n// =========================================\n// COLLISION MECHANISM\n// =========================================\n\nconst CollisionMechanism = React.forwardRef<\n  HTMLDivElement,\n  {\n    containerRef: React.RefObject<HTMLDivElement | null>;\n    parentRef: React.RefObject<HTMLDivElement | null>;\n    beamOptions?: BeamConfig;\n  }\n>(({ parentRef, containerRef, beamOptions = {} as BeamConfig }, _ref) => {\n  const beamRef = useRef<HTMLDivElement>(null);\n  const [collision, setCollision] = useState<CollisionState>({\n    detected: false,\n    coordinates: null,\n  });\n  const [beamKey, setBeamKey] = useState(0);\n  const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false);\n\n  // Collision detection loop\n  useEffect(() => {\n    const checkCollision = () => {\n      if (\n        beamRef.current &&\n        containerRef.current &&\n        parentRef.current &&\n        !cycleCollisionDetected\n      ) {\n        const beamRect = beamRef.current.getBoundingClientRect();\n        const containerRect = containerRef.current.getBoundingClientRect();\n        const parentRect = parentRef.current.getBoundingClientRect();\n\n        // Detect when beam hits the container surface\n        if (beamRect.bottom >= containerRect.top) {\n          const relativeX =\n            beamRect.left - parentRect.left + beamRect.width / 2;\n          const relativeY = beamRect.bottom - parentRect.top;\n\n          setCollision({\n            detected: true,\n            coordinates: { x: relativeX, y: relativeY },\n          });\n          setCycleCollisionDetected(true);\n        }\n      }\n    };\n\n    // Performance: Reduced from 50ms to 200ms (75% less CPU usage)\n    const animationInterval = setInterval(checkCollision, 200);\n    return () => clearInterval(animationInterval);\n  }, [cycleCollisionDetected, containerRef, parentRef]);\n\n  // Reset collision after explosion animation\n  useEffect(() => {\n    if (collision.detected && collision.coordinates) {\n      const explosionDuration = 2000;\n\n      setTimeout(() => {\n        setCollision({ detected: false, coordinates: null });\n        setCycleCollisionDetected(false);\n      }, explosionDuration);\n\n      setTimeout(() => {\n        setBeamKey((prevKey) => prevKey + 1);\n      }, explosionDuration);\n    }\n  }, [collision]);\n\n  return (\n    <>\n      {/* Animated beam */}\n      <motion.div\n        key={beamKey}\n        ref={beamRef}\n        animate=\"animate\"\n        initial={{\n          translateY: beamOptions.initialY || \"-200px\",\n          translateX: beamOptions.initialX || \"0px\",\n          rotate: beamOptions.rotate || 0,\n        }}\n        variants={{\n          animate: {\n            translateY: beamOptions.translateY || \"1800px\",\n            translateX: beamOptions.translateX || \"0px\",\n            rotate: beamOptions.rotate || 0,\n          },\n        }}\n        transition={{\n          duration: beamOptions.duration || 8,\n          repeat: Infinity,\n          repeatType: \"loop\",\n          ease: \"linear\",\n          delay: beamOptions.delay || 0,\n          repeatDelay: beamOptions.repeatDelay || 0,\n        }}\n        className={cn(\n          // Base beam styling\n          \"absolute left-0 top-20 m-auto h-14 w-px rounded-full\",\n          // Brand beam. No dark: variant — the tokens resolve per theme.\n          \"bg-gradient-to-t from-primary via-chart-2 to-transparent\",\n                    beamOptions.className\n        )}\n      />\n\n      {/* Explosion effect on collision */}\n      <AnimatePresence>\n        {collision.detected && collision.coordinates && (\n          <Explosion\n            key={`${collision.coordinates.x}-${collision.coordinates.y}`}\n            style={{\n              left: `${collision.coordinates.x}px`,\n              top: `${collision.coordinates.y}px`,\n              transform: \"translate(-50%, -50%)\",\n            }}\n          />\n        )}\n      </AnimatePresence>\n    </>\n  );\n});\n\nCollisionMechanism.displayName = \"CollisionMechanism\";\n\n// =========================================\n// EXPLOSION EFFECT\n// =========================================\n\n/**\n * Particle explosion animation that triggers on beam collision\n * Performance: Reduced from 20 to 10 particles for 50% less motion overhead\n */\nconst Explosion = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {\n  // Generate particles with random directions using lazy state initializer\n  // This ensures random values are only generated once on mount, not during render\n  const [spans] = React.useState(() =>\n    Array.from({ length: 10 }, (_, index) => ({\n      id: index,\n      initialX: 0,\n      initialY: 0,\n      directionX: Math.floor(Math.random() * 80 - 40),\n      directionY: Math.floor(Math.random() * -50 - 10),\n      duration: Math.random() * 1.5 + 0.5,\n    }))\n  );\n\n  return (\n    <div {...props} className={cn(\"absolute z-50 h-2 w-2\", props.className)}>\n      {/* Horizontal glow line */}\n      <motion.div\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        exit={{ opacity: 0 }}\n        transition={{ duration: 1.5, ease: \"easeOut\" }}\n        className={cn(\n          \"absolute -inset-x-10 top-0 m-auto h-2 w-10 rounded-full blur-sm\",\n          // Brand glow\n          \"bg-gradient-to-r from-transparent via-primary to-transparent\"\n        )}\n      />\n\n      {/* Scattered particles */}\n      {spans.map((span) => (\n        <motion.span\n          key={span.id}\n          initial={{ x: span.initialX, y: span.initialY, opacity: 1 }}\n          animate={{\n            x: span.directionX,\n            y: span.directionY,\n            opacity: 0,\n          }}\n          transition={{\n            duration: span.duration,\n            ease: \"easeOut\",\n          }}\n          className={cn(\n            \"absolute h-1 w-1 rounded-full\",\n            // Brand particles\n            \"bg-gradient-to-b from-primary to-chart-2\"\n          )}\n        />\n      ))}\n    </div>\n  );\n};\n\nexport default BackgroundBeamsWithCollision;\n"
    }
  ],
  "meta": {
    "tier": "effect",
    "client": true,
    "minViewport": null,
    "loading": false,
    "version": "1.2.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/background-beams-with-collision\n\nInstalled to `components/ui/aceternity/background-beams-with-collision.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/aceternity/background-beams-with-collision';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/background-beams-with-collision\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
