{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-beam",
  "title": "Animated Beam",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/animated-beam.tsx",
      "content": "// Ballmac UI: Animated Beam. https://ui.ballmac.com/components/animated-beam\n// Based on Magic UI's Animated Beam (MIT, Copyright (c) Magic UI), rewritten: the pulse is a dash that follows the curve with a head-to-tail gradient, token colors, a glow, off-screen pausing and a static reduced-motion state.\n\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype AnimatedBeamProps = Omit<React.ComponentProps<\"svg\">, \"children\"> & {\n  /** The positioned element both endpoints live in. The beam's SVG covers it. */\n  containerRef: React.RefObject<HTMLElement | null>\n  /** Element the beam starts from (its center, plus the start offsets). */\n  fromRef: React.RefObject<HTMLElement | null>\n  /** Element the beam ends at (its center, plus the end offsets). */\n  toRef: React.RefObject<HTMLElement | null>\n  /** How far the curve bows, in pixels. Positive bends up, negative bends down, 0 is straight. */\n  curvature?: number\n  /** Send the pulse from `toRef` to `fromRef`. */\n  reverse?: boolean\n  /** Seconds for the pulse to travel the path once. */\n  duration?: number\n  /** Seconds before the first pulse. Stagger beams with different delays. */\n  delay?: number\n  /** Seconds to wait between pulses. */\n  repeatDelay?: number\n  /** Length of the pulse as a fraction of the path (0–1). */\n  pulseLength?: number\n  /** Horizontal offset of the start point in pixels. */\n  startXOffset?: number\n  /** Vertical offset of the start point in pixels. */\n  startYOffset?: number\n  /** Horizontal offset of the end point in pixels. */\n  endXOffset?: number\n  /** Vertical offset of the end point in pixels. */\n  endYOffset?: number\n  /** Color of the resting path. Any CSS color or var(). */\n  pathColor?: string\n  /** Stroke width of the path and pulse in pixels. */\n  pathWidth?: number\n  /** Opacity of the resting path (0–1). */\n  pathOpacity?: number\n  /** Color at the tail of the pulse. */\n  gradientStartColor?: string\n  /** Color at the head of the pulse. */\n  gradientStopColor?: string\n}\n\ntype Geometry = { width: number; height: number; d: string }\n\n// Gentle in-out so the pulse accelerates away and settles in, like a signal arriving.\nconst easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2)\n\nfunction AnimatedBeam({\n  containerRef,\n  fromRef,\n  toRef,\n  curvature = 0,\n  reverse = false,\n  duration = 3.5,\n  delay = 0,\n  repeatDelay = 0.8,\n  pulseLength = 0.35,\n  startXOffset = 0,\n  startYOffset = 0,\n  endXOffset = 0,\n  endYOffset = 0,\n  pathColor = \"var(--foreground)\",\n  pathWidth = 2,\n  pathOpacity = 0.1,\n  gradientStartColor = \"var(--chart-1)\",\n  gradientStopColor = \"var(--chart-4)\",\n  className,\n  style,\n  ...props\n}: AnimatedBeamProps) {\n  const uid = React.useId().replace(/[^a-zA-Z0-9_-]/g, \"\")\n  const gradientId = `bm-beam-${uid}`\n  const glowId = `bm-beam-glow-${uid}`\n  const reduceMotion = useReducedMotion()\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const pulseRef = React.useRef<SVGPathElement>(null)\n  const glowRef = React.useRef<SVGPathElement>(null)\n  const gradientRef = React.useRef<SVGLinearGradientElement>(null)\n  const [geometry, setGeometry] = React.useState<Geometry>({ width: 0, height: 0, d: \"\" })\n\n  // Measure the endpoints relative to the container and rebuild the curve on any resize.\n  React.useEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n    const update = () => {\n      const from = fromRef.current\n      const to = toRef.current\n      if (!from || !to) return\n      const box = container.getBoundingClientRect()\n      const a = from.getBoundingClientRect()\n      const b = to.getBoundingClientRect()\n      const sx = a.left - box.left + a.width / 2 + startXOffset\n      const sy = a.top - box.top + a.height / 2 + startYOffset\n      const ex = b.left - box.left + b.width / 2 + endXOffset\n      const ey = b.top - box.top + b.height / 2 + endYOffset\n      const d = `M ${sx},${sy} Q ${(sx + ex) / 2},${(sy + ey) / 2 - curvature} ${ex},${ey}`\n      setGeometry((prev) =>\n        prev.d === d && prev.width === box.width && prev.height === box.height\n          ? prev\n          : { width: box.width, height: box.height, d }\n      )\n    }\n    update()\n    const observer = new ResizeObserver(update)\n    observer.observe(container)\n    if (fromRef.current) observer.observe(fromRef.current)\n    if (toRef.current) observer.observe(toRef.current)\n    return () => observer.disconnect()\n  }, [containerRef, fromRef, toRef, curvature, startXOffset, startYOffset, endXOffset, endYOffset])\n\n  // Drive the pulse from one rAF loop: a dash slides along the path and the gradient follows it.\n  React.useEffect(() => {\n    const svg = svgRef.current\n    const pulse = pulseRef.current\n    const glow = glowRef.current\n    const gradient = gradientRef.current\n    if (!svg || !pulse || !glow || !gradient || !geometry.d || reduceMotion) return\n    if (typeof pulse.getTotalLength !== \"function\") return\n    const total = pulse.getTotalLength()\n    if (!total) return\n    const length = Math.min(Math.max(pulseLength, 0.05), 1)\n    const cycle = duration + repeatDelay\n    const at = (fraction: number) => {\n      const along = Math.min(Math.max(fraction, 0), 1) * total\n      return pulse.getPointAtLength(reverse ? total - along : along)\n    }\n\n    let frame = 0\n    let visible = true\n    let start: number | null = null\n    const paint = (now: number) => {\n      start ??= now\n      const elapsed = (now - start) / 1000 - delay\n      const local = elapsed < 0 ? -1 : (elapsed % cycle) / duration\n      if (local < 0 || local > 1) {\n        pulse.style.opacity = glow.style.opacity = \"0\"\n      } else {\n        const head = easeInOut(local) * (1 + length)\n        const tail = head - length\n        // The dash covers [tail, head] along the path, measured from the travel start.\n        const offset = reverse ? String(-(1 - head)) : String(-tail)\n        pulse.style.strokeDashoffset = glow.style.strokeDashoffset = offset\n        pulse.style.opacity = \"1\"\n        glow.style.opacity = \"0.55\"\n        const t = at(tail)\n        const h = at(head)\n        gradient.setAttribute(\"x1\", String(t.x))\n        gradient.setAttribute(\"y1\", String(t.y))\n        gradient.setAttribute(\"x2\", String(h.x))\n        gradient.setAttribute(\"y2\", String(h.y))\n      }\n      frame = requestAnimationFrame(paint)\n    }\n    const run = () => {\n      cancelAnimationFrame(frame)\n      if (visible && !document.hidden) frame = requestAnimationFrame(paint)\n    }\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry?.isIntersecting ?? true\n      run()\n    })\n    io.observe(svg)\n    document.addEventListener(\"visibilitychange\", run)\n    run()\n    return () => {\n      cancelAnimationFrame(frame)\n      io.disconnect()\n      document.removeEventListener(\"visibilitychange\", run)\n    }\n  }, [geometry.d, reduceMotion, duration, delay, repeatDelay, pulseLength, reverse])\n\n  const staticBeam = reduceMotion === true\n  const dash = staticBeam ? undefined : `${Math.min(Math.max(pulseLength, 0.05), 1)} 3`\n\n  return (\n    <svg\n      ref={svgRef}\n      aria-hidden=\"true\"\n      data-slot=\"animated-beam\"\n      fill=\"none\"\n      width={geometry.width}\n      height={geometry.height}\n      viewBox={`0 0 ${geometry.width} ${geometry.height}`}\n      className={cn(\"pointer-events-none absolute top-0 start-0 overflow-visible\", className)}\n      style={style}\n      {...props}\n    >\n      <defs>\n        <linearGradient\n          ref={gradientRef}\n          id={gradientId}\n          gradientUnits=\"userSpaceOnUse\"\n          x1=\"0\"\n          y1=\"0\"\n          x2={geometry.width}\n          y2=\"0\"\n        >\n          <stop offset=\"0%\" style={{ stopColor: gradientStartColor, stopOpacity: 0 }} />\n          <stop offset=\"35%\" style={{ stopColor: gradientStartColor }} />\n          <stop offset=\"100%\" style={{ stopColor: gradientStopColor }} />\n        </linearGradient>\n        <filter id={glowId} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n          <feGaussianBlur stdDeviation={pathWidth * 1.5} />\n        </filter>\n      </defs>\n      <path\n        data-slot=\"animated-beam-path\"\n        d={geometry.d}\n        strokeWidth={pathWidth}\n        strokeLinecap=\"round\"\n        style={{ stroke: pathColor, strokeOpacity: pathOpacity }}\n      />\n      <path\n        ref={glowRef}\n        d={geometry.d}\n        pathLength={1}\n        stroke={`url(#${gradientId})`}\n        strokeWidth={pathWidth * 3}\n        strokeLinecap=\"round\"\n        strokeDasharray={dash}\n        filter={`url(#${glowId})`}\n        style={{ opacity: 0 }}\n      />\n      <path\n        ref={pulseRef}\n        data-slot=\"animated-beam-pulse\"\n        d={geometry.d}\n        pathLength={1}\n        stroke={`url(#${gradientId})`}\n        strokeWidth={pathWidth}\n        strokeLinecap=\"round\"\n        strokeDasharray={dash}\n        style={{ opacity: staticBeam ? 0.45 : 0 }}\n      />\n    </svg>\n  )\n}\n\nexport { AnimatedBeam, type AnimatedBeamProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/animated-beam.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "beam",
      "connection",
      "integration",
      "diagram",
      "svg",
      "hero",
      "motion"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Give a relative container a ref, give each endpoint element a ref, and render <AnimatedBeam containerRef fromRef toRef /> inside the container once per connection. Put endpoint nodes above the beams with relative z-10. The SVG is aria-hidden and pointer-events-none.",
      "whenToUse": [
        "Integration diagrams: tools or data sources flowing into an agent or product",
        "Showing a request path between services on a landing page",
        "Explaining a sync or pipeline visually in a feature section"
      ],
      "whenNotToUse": [
        "Real, data-driven graphs with many nodes (use a graph library)",
        "Conveying state on its own; label the nodes, the beam is decorative"
      ],
      "composesWith": [
        "orbiting-circles",
        "dot-pattern",
        "border-beam"
      ],
      "a11y": [],
      "customization": [
        "curvature in px (positive bows up), reverse to flip direction",
        "duration, delay and repeatDelay in seconds; stagger delays across beams",
        "pulseLength as a fraction of the path (default 0.35)",
        "startXOffset/startYOffset/endXOffset/endYOffset to attach to node edges",
        "pathColor, pathWidth, pathOpacity; gradientStartColor/gradientStopColor default to var(--chart-1) → var(--chart-4)"
      ]
    },
    "source": {
      "name": "Magic UI Animated Beam",
      "url": "https://github.com/magicuidesign/magicui",
      "license": "MIT",
      "copyright": "Copyright (c) Magic UI",
      "modified": true
    },
    "examples": [
      "animated-beam-demo",
      "animated-beam-bidirectional"
    ],
    "url": "https://ui.ballmac.com/components/animated-beam"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}