{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "conduit",
  "type": "registry:ui",
  "title": "Conduit",
  "description": "An animated beam connecting two DOM nodes via refs — a dark groove path carrying a blue-to-white slug of light that travels into the destination and flashes a white rim-glow on receipt. Curved, reversible, resize-aware.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/utility/conduit/conduit.tsx",
      "type": "registry:ui",
      "target": "components/crucible/conduit.tsx",
      "content": "\"use client\";\n\nimport { motion, type MotionStyle } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\n\nexport interface ConduitProps extends Omit<React.ComponentPropsWithoutRef<\"svg\">, \"children\"> {\n  /** The positioned ancestor both `fromRef` and `toRef` live inside — the SVG overlay fills this. */\n  containerRef: React.RefObject<HTMLElement | null>;\n  /** Source node the beam departs from. */\n  fromRef: React.RefObject<HTMLElement | null>;\n  /** Destination node the beam pours into. */\n  toRef: React.RefObject<HTMLElement | null>;\n  /** Bend amount in px. Positive bows the path downward, negative upward. @default 0 */\n  curvature?: number;\n  /** Reverse the flow so the beam travels from `toRef` back to `fromRef`. @default false */\n  reverse?: boolean;\n  /** Seconds for one full slug travel + loop. @default 3 */\n  duration?: number;\n  /** Seconds before the first pass starts. @default 0 */\n  delay?: number;\n  /** Horizontal/vertical pixel offsets applied to the start point (on `fromRef`). */\n  startXOffset?: number;\n  startYOffset?: number;\n  /** Horizontal/vertical pixel offsets applied to the end point (on `toRef`). */\n  endXOffset?: number;\n  endYOffset?: number;\n  /** Static groove (channel) color. @default \"#2a2a30\" */\n  pathColor?: string;\n  /** Static groove opacity. @default 0.5 */\n  pathOpacity?: number;\n  /** Groove + slug stroke width in px. @default 2 */\n  pathWidth?: number;\n  /** Slug head/body/tail colors, bright to dim. @default [\"#ffffff\", \"#93c5fd\", \"#3b82f6\"] */\n  slugColors?: [string, string, string];\n  /** Flash a brief rim-glow on the receiving node each time the slug arrives. @default true */\n  glowOnReceive?: boolean;\n}\n\ninterface PathState {\n  d: string;\n  endX: number;\n  endY: number;\n  startX: number;\n  startY: number;\n  /** Half the larger dimension of each anchored node — sizes the arrival rim-glow. */\n  startR: number;\n  endR: number;\n}\n\nfunction computePath(\n  container: HTMLElement,\n  from: HTMLElement,\n  to: HTMLElement,\n  curvature: number,\n  startXOffset: number,\n  startYOffset: number,\n  endXOffset: number,\n  endYOffset: number\n): PathState {\n  const containerRect = container.getBoundingClientRect();\n  const fromRect = from.getBoundingClientRect();\n  const toRect = to.getBoundingClientRect();\n\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\n  const midX = (startX + endX) / 2;\n  const midY = (startY + endY) / 2 + curvature;\n\n  return {\n    d: `M ${startX} ${startY} Q ${midX} ${midY} ${endX} ${endY}`,\n    startX,\n    startY,\n    endX,\n    endY,\n    startR: Math.max(fromRect.width, fromRect.height) / 2,\n    endR: Math.max(toRect.width, toRect.height) / 2,\n  };\n}\n\n/**\n * Conduit — an animated beam connecting two DOM nodes, like a signal on a\n * wire: a dark groove path carries a slug of light (white head, blue body,\n * tail tapering to transparent) that travels into the destination node,\n * flashing a brief white rim-glow on receipt. Curved via `curvature`,\n * reversible via `reverse`, and re-anchors on resize.\n *\n * Purely decorative — always `aria-hidden`. Under reduced motion the groove\n * and a static slug render with no travel or flash.\n */\nexport function Conduit({\n  containerRef,\n  fromRef,\n  toRef,\n  curvature = 0,\n  reverse = false,\n  duration = 3,\n  delay = 0,\n  startXOffset = 0,\n  startYOffset = 0,\n  endXOffset = 0,\n  endYOffset = 0,\n  pathColor = \"#2a2a30\",\n  pathOpacity = 0.5,\n  pathWidth = 2,\n  slugColors = [\"#ffffff\", \"#93c5fd\", \"#3b82f6\"],\n  glowOnReceive = true,\n  className,\n  ...props\n}: ConduitProps) {\n  const reducedMotion = useReducedMotion();\n  const svgRef = React.useRef<SVGSVGElement>(null);\n  const [dims, setDims] = React.useState({ width: 0, height: 0 });\n  const [path, setPath] = React.useState<PathState | null>(null);\n  const reactId = React.useId();\n  const gradientId = `conduit-gradient-${reactId}`;\n  const bloomId = `conduit-bloom-${reactId}`;\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 update = () => {\n      const rect = container.getBoundingClientRect();\n      setDims({ width: rect.width, height: rect.height });\n      setPath(computePath(container, from, to, curvature, startXOffset, startYOffset, endXOffset, endYOffset));\n    };\n\n    update();\n\n    const ro = new ResizeObserver(update);\n    ro.observe(container);\n    ro.observe(from);\n    ro.observe(to);\n    window.addEventListener(\"resize\", update);\n\n    return () => {\n      ro.disconnect();\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [containerRef, fromRef, toRef, curvature, startXOffset, startYOffset, endXOffset, endYOffset]);\n\n  if (!path) {\n    return (\n      <svg\n        ref={svgRef}\n        aria-hidden\n        data-crucible=\"conduit\"\n        className={cn(\"pointer-events-none absolute inset-0\", className)}\n        width={dims.width}\n        height={dims.height}\n        {...props}\n      />\n    );\n  }\n\n  const arrivalPoint = reverse ? { x: path.startX, y: path.startY } : { x: path.endX, y: path.endY };\n  // Rim-glow radius tracks the receiving node's size so the bloom reads as a\n  // halo around the node (typically layered above the beam), not a dot on it.\n  const arrivalR = (reverse ? path.startR : path.endR) || pathWidth * 4;\n\n  return (\n    <svg\n      ref={svgRef}\n      aria-hidden\n      data-crucible=\"conduit\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-visible\", className)}\n      width={dims.width}\n      height={dims.height}\n      {...props}\n    >\n      <defs>\n        {/* userSpaceOnUse: an objectBoundingBox gradient degenerates to nothing\n            on perfectly horizontal/vertical paths (zero-area box). Runs tail → head\n            along the direction of travel. */}\n        <linearGradient\n          id={gradientId}\n          gradientUnits=\"userSpaceOnUse\"\n          x1={reverse ? path.endX : path.startX}\n          y1={reverse ? path.endY : path.startY}\n          x2={reverse ? path.startX : path.endX}\n          y2={reverse ? path.startY : path.endY}\n        >\n          <stop offset=\"0%\" stopColor={slugColors[2]} stopOpacity={0} />\n          <stop offset=\"42%\" stopColor={slugColors[2]} stopOpacity={0.6} />\n          <stop offset=\"80%\" stopColor={slugColors[1]} />\n          <stop offset=\"100%\" stopColor={slugColors[0]} />\n        </linearGradient>\n        <filter id={bloomId} x=\"-150%\" y=\"-150%\" width=\"400%\" height=\"400%\">\n          <feGaussianBlur stdDeviation={Math.max(arrivalR * 0.35, 4)} />\n        </filter>\n      </defs>\n\n      {/* Static channel groove */}\n      <path d={path.d} fill=\"none\" stroke={pathColor} strokeOpacity={pathOpacity} strokeWidth={pathWidth} strokeLinecap=\"round\" />\n\n      {reducedMotion ? (\n        // Static slug resting mid-channel — no travel, no flash.\n        <motion.path\n          d={path.d}\n          fill=\"none\"\n          stroke={slugColors[1]}\n          strokeWidth={pathWidth}\n          strokeLinecap=\"round\"\n          // pathLength/pathOffset must live in initial/animate (not style) or\n          // motion never emits the dasharray that makes the slug a segment.\n          initial={{ pathLength: 0.3, pathOffset: 0.35 }}\n          animate={{ pathLength: 0.3, pathOffset: 0.35 }}\n          style={{ opacity: 0.85 } as MotionStyle}\n        />\n      ) : (\n        <>\n          <motion.path\n            d={path.d}\n            fill=\"none\"\n            stroke={`url(#${gradientId})`}\n            strokeWidth={pathWidth * 1.4}\n            strokeLinecap=\"round\"\n            style={{ filter: `drop-shadow(0 0 ${pathWidth * 3}px ${slugColors[1]}aa)` }}\n            // pathLength must live in initial/animate (not as an attribute) or\n            // motion never emits the dasharray that makes the slug a segment.\n            initial={{ pathLength: 0.16, pathOffset: reverse ? 1 : 0 }}\n            animate={{ pathLength: 0.16, pathOffset: reverse ? [1, 0] : [0, 1] }}\n            transition={{ duration, delay, repeat: Infinity, ease: \"linear\" }}\n          />\n          {glowOnReceive && (\n            <>\n              {/* Node-sized rim bloom — visible as a halo even when the\n                  receiving node is layered above the beam overlay. */}\n              <motion.circle\n                cx={arrivalPoint.x}\n                cy={arrivalPoint.y}\n                r={arrivalR * 1.05}\n                fill={slugColors[1]}\n                filter={`url(#${bloomId})`}\n                animate={{ opacity: [0, 0, 0.45, 0], scale: [0.9, 0.9, 1.1, 1.25] }}\n                transition={{\n                  duration,\n                  delay,\n                  repeat: Infinity,\n                  ease: \"easeOut\",\n                  times: [0, 0.86, 0.93, 1],\n                }}\n                style={{ transformOrigin: `${arrivalPoint.x}px ${arrivalPoint.y}px` }}\n              />\n              {/* Bright point flash at the exact arrival point. */}\n              <motion.circle\n                cx={arrivalPoint.x}\n                cy={arrivalPoint.y}\n                r={pathWidth * 2}\n                fill={slugColors[0]}\n                animate={{ opacity: [0, 0, 1, 0], scale: [0.6, 0.6, 1.6, 2.2] }}\n                transition={{\n                  duration,\n                  delay,\n                  repeat: Infinity,\n                  ease: \"easeOut\",\n                  times: [0, 0.86, 0.94, 1],\n                }}\n                style={{ transformOrigin: `${arrivalPoint.x}px ${arrivalPoint.y}px` }}\n              />\n            </>\n          )}\n        </>\n      )}\n    </svg>\n  );\n}\n"
    }
  ]
}