{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-beam",
  "title": "Animated Beam",
  "description": "Connect two interface nodes with a responsive animated SVG beam.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/animated-beam.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nconst KEYFRAMES = `@keyframes zab-travel{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}`\n\ninterface BeamGeometry {\n  width: number\n  height: number\n  path: string\n}\n\nconst EMPTY_GEOMETRY: BeamGeometry = { width: 0, height: 0, path: \"\" }\n\nexport interface AnimatedBeamProps\n  extends Omit<React.SVGAttributes<SVGSVGElement>, \"children\"> {\n  /** Positioned ancestor that defines the beam's coordinate system. */\n  containerRef: React.RefObject<HTMLElement | null>\n  /** Element whose visual center anchors the start of the path. */\n  fromRef: React.RefObject<HTMLElement | null>\n  /** Element whose visual center anchors the end of the path. */\n  toRef: React.RefObject<HTMLElement | null>\n  /** Vertical displacement, in px, of the quadratic Bézier control point. */\n  curvature?: number\n  /** Play the traveling segment from destination back to source. */\n  reverse?: boolean\n  /** Seconds for one complete pass; deterministic by default. */\n  duration?: number\n  /** Initial animation delay in seconds. */\n  delay?: number\n  /** Stroke width for both the track and traveling segment. */\n  pathWidth?: number\n  /** Fraction of the normalized path occupied by the traveling segment. */\n  beamLength?: number\n  /**\n   * Change this value after a position-only layout update (for example a CSS\n   * transform) that does not resize or scroll any observed element.\n   */\n  refreshKey?: React.Key\n  startXOffset?: number\n  startYOffset?: number\n  endXOffset?: number\n  endYOffset?: number\n  /** Semantic stroke classes for the quiet route underneath the beam. */\n  trackClassName?: string\n  /** Semantic stroke classes for the traveling highlight. */\n  beamClassName?: string\n}\n\n/**\n * Draws a responsive quadratic connection between two referenced elements.\n * The SVG is decorative; source and destination elements carry the semantics.\n */\nexport function AnimatedBeam({\n  containerRef,\n  fromRef,\n  toRef,\n  curvature = 0,\n  reverse = false,\n  duration = 3.6,\n  delay = 0,\n  pathWidth = 2,\n  beamLength = 0.22,\n  refreshKey,\n  startXOffset = 0,\n  startYOffset = 0,\n  endXOffset = 0,\n  endYOffset = 0,\n  trackClassName,\n  beamClassName,\n  className,\n  style,\n  ...props\n}: AnimatedBeamProps) {\n  const [geometry, setGeometry] = React.useState<BeamGeometry>(EMPTY_GEOMETRY)\n\n  React.useEffect(() => {\n    let frame = 0\n    const observedElements = new Set<HTMLElement>()\n    let observer: ResizeObserver | null = null\n\n    const syncObservedElements = (elements: HTMLElement[]) => {\n      if (!observer) return\n\n      const nextElements = new Set(elements)\n      for (const element of observedElements) {\n        if (!nextElements.has(element)) {\n          observer.unobserve(element)\n          observedElements.delete(element)\n        }\n      }\n      for (const element of nextElements) {\n        if (!observedElements.has(element)) {\n          observer.observe(element)\n          observedElements.add(element)\n        }\n      }\n    }\n\n    const measure = () => {\n      frame = 0\n\n      // Re-read current on every pass: refs may be retargeted without changing\n      // the ref object's identity, especially across conditional layouts.\n      const container = containerRef.current\n      const from = fromRef.current\n      const to = toRef.current\n      const currentElements = [container, from, to].filter(\n        (element): element is HTMLElement => element !== null,\n      )\n      syncObservedElements(currentElements)\n\n      if (!container || !from || !to) {\n        setGeometry(previous =>\n          previous.path === \"\" ? previous : EMPTY_GEOMETRY,\n        )\n        return\n      }\n\n      const containerRect = container.getBoundingClientRect()\n      const fromRect = from.getBoundingClientRect()\n      const toRect = to.getBoundingClientRect()\n\n      const width = Math.max(0, containerRect.width)\n      const height = Math.max(0, containerRect.height)\n      const startX = fromRect.left - containerRect.left + fromRect.width / 2 + startXOffset\n      const startY = fromRect.top - containerRect.top + fromRect.height / 2 + startYOffset\n      const endX = toRect.left - containerRect.left + toRect.width / 2 + endXOffset\n      const endY = toRect.top - containerRect.top + toRect.height / 2 + endYOffset\n      const controlX = (startX + endX) / 2\n      const controlY = (startY + endY) / 2 - curvature\n      const path = `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`\n\n      setGeometry(previous =>\n        previous.width === width && previous.height === height && previous.path === path\n          ? previous\n          : { width, height, path },\n      )\n    }\n\n    const scheduleMeasure = () => {\n      if (frame !== 0) cancelAnimationFrame(frame)\n      frame = requestAnimationFrame(measure)\n    }\n\n    observer =\n      typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(scheduleMeasure)\n    window.addEventListener(\"resize\", scheduleMeasure)\n    // Scroll does not resize any observed box, but it can move an endpoint\n    // relative to a fixed/sticky container. Capture catches nested scrollers.\n    window.addEventListener(\"scroll\", scheduleMeasure, true)\n    scheduleMeasure()\n\n    return () => {\n      if (frame !== 0) cancelAnimationFrame(frame)\n      observer?.disconnect()\n      observedElements.clear()\n      window.removeEventListener(\"resize\", scheduleMeasure)\n      window.removeEventListener(\"scroll\", scheduleMeasure, true)\n    }\n  }, [\n    containerRef,\n    fromRef,\n    toRef,\n    curvature,\n    refreshKey,\n    startXOffset,\n    startYOffset,\n    endXOffset,\n    endYOffset,\n  ])\n\n  const safeDuration = Number.isFinite(duration) ? Math.max(0.2, duration) : 3.6\n  const safeDelay = Number.isFinite(delay) ? Math.max(0, delay) : 0\n  const safeLength = Number.isFinite(beamLength)\n    ? Math.min(0.8, Math.max(0.05, beamLength))\n    : 0.22\n\n  return (\n    <svg\n      {...props}\n      aria-hidden=\"true\"\n      className={cn(\"pointer-events-none absolute inset-0 size-full overflow-visible\", className)}\n      focusable=\"false\"\n      height={geometry.height}\n      preserveAspectRatio=\"none\"\n      style={\n        {\n          \"--zab-duration\": `${safeDuration}s`,\n          \"--zab-delay\": `${safeDelay}s`,\n          ...style,\n        } as React.CSSProperties\n      }\n      viewBox={`0 0 ${Math.max(1, geometry.width)} ${Math.max(1, geometry.height)}`}\n      width={geometry.width}\n    >\n      <style href=\"zyeon-animated-beam\" precedence=\"medium\">\n        {KEYFRAMES}\n      </style>\n\n      <path\n        className={cn(\"stroke-border\", trackClassName)}\n        d={geometry.path}\n        fill=\"none\"\n        strokeLinecap=\"round\"\n        strokeWidth={pathWidth}\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      <path\n        className={cn(\n          \"stroke-primary\",\n          \"[animation:zab-travel_var(--zab-duration)_linear_var(--zab-delay)_infinite]\",\n          \"motion-reduce:hidden\",\n          beamClassName,\n        )}\n        d={geometry.path}\n        fill=\"none\"\n        pathLength=\"1\"\n        strokeDasharray={`${safeLength} ${1 - safeLength}`}\n        strokeLinecap=\"round\"\n        strokeWidth={pathWidth}\n        style={{ animationDirection: reverse ? \"reverse\" : \"normal\" }}\n        vectorEffect=\"non-scaling-stroke\"\n      />\n    </svg>\n  )\n}\n\nexport default AnimatedBeam\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}