{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "connection-beam",
  "title": "Connection Beam",
  "description": "A line between two elements with something travelling along it, measured from the elements rather than given as coordinates.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/connection-beam/connection-beam.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type ConnectionBeamProps = Omit<\n  React.SVGAttributes<SVGSVGElement>,\n  \"children\"\n> & {\n  /** The positioned box both endpoints live inside. */\n  containerRef: React.RefObject<HTMLElement | null>\n  fromRef: React.RefObject<HTMLElement | null>\n  toRef: React.RefObject<HTMLElement | null>\n  /** How far the path bows away from the straight line, in pixels. */\n  curvature?: number\n  /** Which edges to leave from and arrive at. Auto picks by the longer axis. */\n  anchor?: \"auto\" | \"horizontal\" | \"vertical\"\n  /** Pixels of clearance left at each end, so the path does not touch. */\n  inset?: number\n  /** The line that is always there. A theme token, or any CSS colour. */\n  pathColor?: string\n  /** The part that travels. */\n  beamColor?: string\n  width?: number\n  /** Seconds for one trip. */\n  duration?: number\n  /** Seconds before the first trip. */\n  delay?: number\n  /** Sends it from the second element to the first. */\n  reverse?: boolean\n  /** How much of the path the travelling part covers, from nought to one. */\n  extent?: number\n}\n\ntype Geometry = { width: number; height: number; d: string }\n\n/** Where a beam should leave a box, given which way it is heading. */\nfunction edgePoint(\n  rect: DOMRect,\n  origin: DOMRect,\n  towards: DOMRect,\n  horizontal: boolean,\n  inset: number\n) {\n  const x = rect.left - origin.left\n  const y = rect.top - origin.top\n  const midX = x + rect.width / 2\n  const midY = y + rect.height / 2\n\n  if (horizontal) {\n    const rightwards =\n      towards.left + towards.width / 2 > rect.left + rect.width / 2\n    return {\n      x: rightwards ? x + rect.width + inset : x - inset,\n      y: midY,\n    }\n  }\n\n  const downwards =\n    towards.top + towards.height / 2 > rect.top + rect.height / 2\n  return {\n    x: midX,\n    y: downwards ? y + rect.height + inset : y - inset,\n  }\n}\n\n/**\n * A line drawn between two elements, with something travelling along it.\n *\n * The geometry is read from the elements themselves rather than given as\n * coordinates, so the diagram survives reflow: change the layout, resize the\n * window, wrap the boxes onto another line, and the beam follows.\n */\nexport function ConnectionBeam({\n  containerRef,\n  fromRef,\n  toRef,\n  curvature = 60,\n  anchor = \"auto\",\n  inset = 4,\n  pathColor = \"--border\",\n  beamColor = \"--primary\",\n  width = 2,\n  duration = 3,\n  delay = 0,\n  reverse = false,\n  extent = 0.18,\n  className,\n  style,\n  ...svgProps\n}: ConnectionBeamProps) {\n  const [geometry, setGeometry] = React.useState<Geometry | null>(null)\n\n  React.useEffect(() => {\n    const container = containerRef.current\n    const from = fromRef.current\n    const to = toRef.current\n    if (!container || !from || !to) return\n\n    const measure = () => {\n      const origin = container.getBoundingClientRect()\n      const a = from.getBoundingClientRect()\n      const b = to.getBoundingClientRect()\n\n      const horizontal =\n        anchor === \"auto\"\n          ? Math.abs(b.left + b.width / 2 - (a.left + a.width / 2)) >=\n            Math.abs(b.top + b.height / 2 - (a.top + a.height / 2))\n          : anchor === \"horizontal\"\n\n      const start = edgePoint(a, origin, b, horizontal, inset)\n      const end = edgePoint(b, origin, a, horizontal, inset)\n\n      // The control point sits off the midpoint, square to the run, so the\n      // bow is the same whichever way round the two elements are.\n      const midX = (start.x + end.x) / 2\n      const midY = (start.y + end.y) / 2\n      const controlX = horizontal ? midX : midX + curvature\n      const controlY = horizontal ? midY - curvature : midY\n\n      setGeometry({\n        width: origin.width,\n        height: origin.height,\n        d: `M ${start.x},${start.y} Q ${controlX},${controlY} ${end.x},${end.y}`,\n      })\n    }\n\n    measure()\n\n    // Either endpoint moving, or the box around them changing shape, moves\n    // the line. Watching all three covers reflow without polling.\n    const observer = new ResizeObserver(measure)\n    observer.observe(container)\n    observer.observe(from)\n    observer.observe(to)\n\n    window.addEventListener(\"resize\", measure)\n    return () => {\n      observer.disconnect()\n      window.removeEventListener(\"resize\", measure)\n    }\n  }, [anchor, containerRef, curvature, fromRef, inset, toRef])\n\n  if (!geometry) return null\n\n  const colour = (value: string) =>\n    value.startsWith(\"--\") ? `var(${value})` : value\n\n  // pathLength normalises the dashes, so the travelling part is a share of the\n  // line rather than a number of pixels that means something different on\n  // every screen.\n  const covered = Math.min(Math.max(extent, 0.01), 1) * 100\n\n  return (\n    <svg\n      data-slot=\"connection-beam\"\n      aria-hidden=\"true\"\n      width={geometry.width}\n      height={geometry.height}\n      viewBox={`0 0 ${geometry.width} ${geometry.height}`}\n      fill=\"none\"\n      className={cn(\"pointer-events-none absolute top-0 left-0\", className)}\n      style={style}\n      {...svgProps}\n    >\n      <path\n        d={geometry.d}\n        stroke={colour(pathColor)}\n        strokeWidth={width}\n        strokeLinecap=\"round\"\n      />\n      <path\n        d={geometry.d}\n        stroke={colour(beamColor)}\n        strokeWidth={width}\n        strokeLinecap=\"round\"\n        pathLength={100}\n        strokeDasharray={`${covered} ${100 - covered}`}\n        className={cn(\n          \"motion-safe:animate-[mischief-connection-beam_var(--beam-duration)_linear_infinite]\",\n          // Standing still, the travelling part would just be a stray dash.\n          \"motion-reduce:hidden\",\n          reverse && \"[animation-direction:reverse]\"\n        )}\n        style={\n          {\n            \"--beam-duration\": `${duration}s`,\n            animationDelay: `${delay}s`,\n            strokeDashoffset: 100,\n          } as React.CSSProperties\n        }\n      />\n    </svg>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    "@keyframes mischief-connection-beam": {
      "to": {
        "stroke-dashoffset": "0"
      }
    }
  },
  "categories": [
    "scene",
    "canvas",
    "diagram",
    "agent",
    "background"
  ],
  "type": "registry:ui"
}