{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-beam",
  "type": "registry:ui",
  "title": "Animated Beam",
  "description": "The AnimatedBeam component draws a curved line between any two elements and sends a gradient pulse along it, remeasuring whenever the layout moves.",
  "author": "Claude Code (https://claude.com/claude-code)",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/demo/background/animated-beam.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useId, useState, type RefObject } from \"react\";\n\ninterface AnimatedBeamProps {\n  /** The positioned element both refs live inside. */\n  containerRef: RefObject<HTMLElement | null>;\n  fromRef: RefObject<HTMLElement | null>;\n  toRef: RefObject<HTMLElement | null>;\n  /** Arc height in pixels. Positive bows up, negative bows down, 0 is straight. */\n  curvature?: number;\n  /** Send the pulse from `toRef` to `fromRef` instead. */\n  reverse?: boolean;\n  duration?: number;\n  delay?: number;\n  pathColor?: string;\n  gradientStart?: string;\n  gradientStop?: string;\n  className?: string;\n}\n\n/**\n * Draws a curved beam between two elements and sends a gradient pulse along\n * it.\n *\n * The path is measured from the live geometry of the two elements rather than\n * hardcoded, so it survives responsive reflow, font loading and content\n * changes — a `ResizeObserver` on the container recomputes it whenever\n * anything moves.\n */\nexport function AnimatedBeam({\n  containerRef,\n  fromRef,\n  toRef,\n  curvature = 0,\n  reverse = false,\n  duration = 3,\n  delay = 0,\n  pathColor = \"var(--border)\",\n  gradientStart = \"var(--brand)\",\n  gradientStop = \"var(--accent-pink)\",\n  className = \"\",\n}: AnimatedBeamProps) {\n  // `useId` keeps the gradient id unique — two beams on one page sharing an id\n  // would both resolve to whichever was defined last.\n  const gradientId = useId();\n  const [path, setPath] = useState(\"\");\n  const [size, setSize] = useState({ width: 0, height: 0 });\n  const prefersReducedMotion = useReducedMotion();\n\n  const measure = useCallback(() => {\n    const container = containerRef.current;\n    const from = fromRef.current;\n    const to = toRef.current;\n    if (!container || !from || !to) return;\n\n    const containerRect = container.getBoundingClientRect();\n    const fromRect = from.getBoundingClientRect();\n    const toRect = to.getBoundingClientRect();\n\n    setSize({ width: containerRect.width, height: containerRect.height });\n\n    // Centres, expressed relative to the container's own coordinate space.\n    const startX = fromRect.left - containerRect.left + fromRect.width / 2;\n    const startY = fromRect.top - containerRect.top + fromRect.height / 2;\n    const endX = toRect.left - containerRect.left + toRect.width / 2;\n    const endY = toRect.top - containerRect.top + toRect.height / 2;\n\n    const controlX = (startX + endX) / 2;\n    const controlY = (startY + endY) / 2 - curvature;\n\n    setPath(`M ${startX},${startY} Q ${controlX},${controlY} ${endX},${endY}`);\n  }, [containerRef, fromRef, toRef, curvature]);\n\n  useEffect(() => {\n    measure();\n\n    const container = containerRef.current;\n    if (!container) return;\n\n    const observer = new ResizeObserver(measure);\n    observer.observe(container);\n    // Watch the endpoints too: the container can hold its size while the\n    // elements inside it move.\n    if (fromRef.current) observer.observe(fromRef.current);\n    if (toRef.current) observer.observe(toRef.current);\n\n    return () => observer.disconnect();\n  }, [measure, containerRef, fromRef, toRef]);\n\n  if (!path) return null;\n\n  return (\n    <svg\n      fill=\"none\"\n      width={size.width}\n      height={size.height}\n      viewBox={`0 0 ${size.width} ${size.height}`}\n      aria-hidden=\"true\"\n      className={`pointer-events-none absolute top-0 left-0 ${className}`}\n    >\n      <path\n        d={path}\n        stroke={pathColor}\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        strokeOpacity={0.4}\n      />\n      <path\n        d={path}\n        stroke={`url(#${gradientId})`}\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n      />\n      <defs>\n        <motion.linearGradient\n          id={gradientId}\n          gradientUnits=\"userSpaceOnUse\"\n          // A narrow gradient window slid across the path is what reads as a\n          // pulse travelling along the wire.\n          initial={{ x1: \"0%\", x2: \"0%\", y1: \"0%\", y2: \"0%\" }}\n          animate={\n            prefersReducedMotion\n              ? { x1: \"0%\", x2: \"100%\", y1: \"0%\", y2: \"0%\" }\n              : {\n                  x1: reverse ? [\"100%\", \"-10%\"] : [\"-10%\", \"100%\"],\n                  x2: reverse ? [\"110%\", \"0%\"] : [\"0%\", \"110%\"],\n                  y1: [\"0%\", \"0%\"],\n                  y2: [\"0%\", \"0%\"],\n                }\n          }\n          transition={{\n            duration,\n            delay,\n            repeat: prefersReducedMotion ? 0 : Infinity,\n            repeatDelay: 0.4,\n            ease: \"easeInOut\",\n          }}\n        >\n          <stop stopColor={gradientStart} stopOpacity=\"0\" />\n          <stop stopColor={gradientStart} />\n          <stop offset=\"32.5%\" stopColor={gradientStop} />\n          <stop offset=\"100%\" stopColor={gradientStop} stopOpacity=\"0\" />\n        </motion.linearGradient>\n      </defs>\n    </svg>\n  );\n}\n\nexport default AnimatedBeam;\n",
      "type": "registry:ui",
      "target": "components/ui/animated-beam.tsx"
    }
  ]
}
