{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-flow",
  "title": "Agent Flow",
  "description": "A draggable, pannable node-graph that visualizes an agent workflow — labelled nodes with live run status and data packets that flow along the edges between them.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/agent-flow/agent-flow.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\n// GodUI motion language (values mirrored inline — see motion/tokens.ts).\nconst EASE_OUT = [0.22, 1, 0.36, 1] as const;\n// Length-driven flow is linear (constant speed) so the border trace and the\n// beams never speed up or settle — see EASE.linear in motion/tokens.ts.\nconst EASE_LINEAR = [0, 0, 1, 1] as const;\n// Crisp pop for the icon chip — SPRING.snappy in motion/tokens.ts.\nconst SPRING_SNAPPY = { type: \"spring\", stiffness: 520, damping: 32 } as const;\n// Ambient flow speed in px/second (FLOW_SPEED.base). Every length-driven\n// element derives its duration as `length / FLOW_SPEED`, so a card border and\n// the beam that continues from it move at the exact same pace — one flow.\nconst FLOW_SPEED = 280;\n\nexport type AgentNodeStatus = \"idle\" | \"running\" | \"done\" | \"error\";\n\nexport type AgentFlowNode = {\n  id: string;\n  /** Primary label shown on the node. */\n  label: string;\n  /** Secondary label — tool name, model, duration, … */\n  sublabel?: string;\n  /** Consumer-supplied glyph rendered in the icon chip. */\n  icon?: React.ReactNode;\n  status?: AgentNodeStatus;\n  /** Node **center** in canvas units. */\n  x: number;\n  y: number;\n};\n\nexport type AgentFlowEdge = {\n  id: string;\n  /** Source node id. */\n  from: string;\n  /** Target node id. */\n  to: string;\n  /** Animate a data packet along the edge (default `true`). */\n  animated?: boolean;\n  /**\n   * Repeat the packet forever (default `true`). Set `false` to send a single\n   * packet — it travels once and fires `onEdgeArrive` when it lands, which lets\n   * you sequence the graph (light the next node, start the next edge, …).\n   */\n  loop?: boolean;\n  /** Bow of the curve in pixels (positive bends upward). */\n  curvature?: number;\n  /**\n   * Keep the edge lit after its packet passes (default `false`). The line draws\n   * on as the packet travels and then stays lit — the same behaviour as a card's\n   * traced border — so a completed path reads as an established connection.\n   */\n  persist?: boolean;\n};\n\nexport type AgentFlowProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  nodes: AgentFlowNode[];\n  edges: AgentFlowEdge[];\n  /** Allow dragging nodes (default `true`). */\n  draggable?: boolean;\n  /** Allow panning the canvas by dragging the backdrop (default `true`). */\n  pannable?: boolean;\n  /** Seconds for one packet travel (default `3`). */\n  flowDuration?: number;\n  /** Center the graph in view on mount (default `true`). */\n  fitView?: boolean;\n  /**\n   * Auto-play the graph as a continuous light: each node traces its border on\n   * from the left-edge centre out to both sides, its icon lights, then the beam\n   * flows down the outgoing edges to the next node, which traces in turn. Root\n   * nodes (no incoming edge) start the sequence. Overrides `node.status` and\n   * `edge.animated` while on.\n   */\n  autoPlay?: boolean;\n  /** With `autoPlay`, loop the whole sequence instead of stopping at the leaves. */\n  continuous?: boolean;\n  /**\n   * Speed of the continuous light in **px/second** (default `280`). Every\n   * length-driven element — each card's traced border and every beam — runs at\n   * this one pace (`duration = length / flowSpeed`), so the light never changes\n   * speed at the card→line seam. Bigger cards trace for longer, longer edges\n   * flow for longer; the pace stays constant.\n   */\n  flowSpeed?: number;\n  /**\n   * Called when a non-looping edge's packet reaches its target node. Use it to\n   * sync node status with the flow — e.g. light a node the moment its packet\n   * arrives, then start the next edge.\n   */\n  onEdgeArrive?: (edgeId: string) => void;\n  /** Called when a node finishes tracing its border (its icon lights). */\n  onNodeActivate?: (nodeId: string) => void;\n};\n\ntype Point = { x: number; y: number };\ntype Size = { w: number; h: number };\n\nconst FALLBACK_SIZE: Size = { w: 168, h: 64 };\n\nconst STATUS_CHIP: Record<AgentNodeStatus, string> = {\n  idle: \"border-border bg-muted text-muted-foreground\",\n  running: \"border-primary bg-primary/10 text-primary\",\n  done: \"border-primary bg-primary text-primary-foreground\",\n  error: \"border-destructive bg-destructive text-white\",\n};\n\nconst STATUS_CARD: Record<AgentNodeStatus, string> = {\n  // The lit border is drawn by the traced SVG outline overlay, so the card's\n  // own border stays neutral (otherwise a full primary border would hide the\n  // growth). Done keeps a soft ambient glow.\n  idle: \"border-border\",\n  running: \"border-border\",\n  done: \"border-border shadow-[0_0_16px_-10px_var(--primary)]\",\n  error: \"border-destructive/60\",\n};\n\n/** Rounded-rect corner radius (matches the card's `rounded-xl` = 12px). */\nconst CARD_RADIUS = 12;\n\n/**\n * Two symmetric outline paths for a card of size `w`×`h`, both starting at the\n * left-edge centre so the border can trace on in both directions and meet at\n * the right-edge centre — where the outgoing edge begins.\n */\nfunction borderTracePaths(w: number, h: number): [string, string] {\n  const r = Math.min(CARD_RADIUS, w / 2, h / 2);\n  const top = `M 0,${h / 2} L 0,${r} A ${r},${r} 0 0 1 ${r},0 L ${w - r},0 A ${r},${r} 0 0 1 ${w},${r} L ${w},${h / 2}`;\n  const bottom = `M 0,${h / 2} L 0,${h - r} A ${r},${r} 0 0 0 ${r},${h} L ${w - r},${h} A ${r},${r} 0 0 0 ${w},${h - r} L ${w},${h / 2}`;\n  return [top, bottom];\n}\n\n/**\n * Length of one border-trace outline (left-edge centre → over the top →\n * right-edge centre): two verticals, two quarter-arcs and the top run. Dividing\n * it by `flowSpeed` gives the trace duration, so the border front and the beams\n * that continue from it move at the same px/second — one continuous light.\n */\nfunction borderOutlineLength(w: number, h: number): number {\n  const r = Math.min(CARD_RADIUS, w / 2, h / 2);\n  return w + h - (4 - Math.PI) * r;\n}\n\n// Auto-play sequencer state: nodes currently drawing their border, nodes whose\n// border is fully lit, and edges whose packet is in flight.\ntype SeqState = {\n  tracing: Set<string>;\n  lit: Set<string>;\n  flowing: Set<string>;\n};\ntype SeqAction =\n  | { type: \"reset\"; roots: string[] }\n  | { type: \"traceDone\"; id: string; outEdges: string[] }\n  | { type: \"arrive\"; edgeId: string; target: string };\n\nconst EMPTY_SEQ: SeqState = {\n  tracing: new Set(),\n  lit: new Set(),\n  flowing: new Set(),\n};\n\nfunction seqReducer(state: SeqState, action: SeqAction): SeqState {\n  switch (action.type) {\n    case \"reset\":\n      return {\n        tracing: new Set(action.roots),\n        lit: new Set(),\n        flowing: new Set(),\n      };\n    case \"traceDone\": {\n      if (state.lit.has(action.id)) return state;\n      const tracing = new Set(state.tracing);\n      tracing.delete(action.id);\n      const lit = new Set(state.lit).add(action.id);\n      const flowing = new Set(state.flowing);\n      for (const e of action.outEdges) flowing.add(e);\n      return { tracing, lit, flowing };\n    }\n    case \"arrive\": {\n      const flowing = new Set(state.flowing);\n      flowing.delete(action.edgeId);\n      let tracing = state.tracing;\n      if (!state.lit.has(action.target) && !state.tracing.has(action.target)) {\n        tracing = new Set(state.tracing).add(action.target);\n      }\n      return { tracing, lit: state.lit, flowing };\n    }\n    default:\n      return state;\n  }\n}\n\nconst AgentFlow = React.forwardRef<HTMLDivElement, AgentFlowProps>(\n  (\n    {\n      nodes,\n      edges,\n      draggable = true,\n      pannable = true,\n      flowDuration = 3,\n      fitView = true,\n      autoPlay = false,\n      continuous = false,\n      flowSpeed = FLOW_SPEED,\n      onEdgeArrive,\n      onNodeActivate,\n      className,\n      \"aria-label\": ariaLabel,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduce = useReducedMotion();\n    const containerRef = React.useRef<HTMLDivElement | null>(null);\n    const nodeEls = React.useRef(new Map<string, HTMLElement>());\n\n    // Live node centers, seeded from props. Dragging mutates these so edges\n    // follow without any DOM measurement of position.\n    const [positions, setPositions] = React.useState<Record<string, Point>>(\n      () => Object.fromEntries(nodes.map((n) => [n.id, { x: n.x, y: n.y }])),\n    );\n    const [sizes, setSizes] = React.useState<Record<string, Size>>({});\n    const [pan, setPan] = React.useState<Point>({ x: 0, y: 0 });\n    const [scale, setScale] = React.useState(1);\n    const [fitted, setFitted] = React.useState(false);\n\n    // Keep positions in sync when the node set changes (add/remove/reset).\n    React.useEffect(() => {\n      setPositions((prev) => {\n        const next: Record<string, Point> = {};\n        for (const n of nodes) next[n.id] = prev[n.id] ?? { x: n.x, y: n.y };\n        return next;\n      });\n    }, [nodes]);\n\n    const posOf = React.useCallback(\n      (id: string): Point => positions[id] ?? { x: 0, y: 0 },\n      [positions],\n    );\n    const sizeOf = React.useCallback(\n      (id: string): Size => sizes[id] ?? FALLBACK_SIZE,\n      [sizes],\n    );\n\n    // Measure node cards so edge anchors sit on their left/right centers.\n    const registerNode = React.useCallback(\n      (id: string) => (el: HTMLElement | null) => {\n        if (el) nodeEls.current.set(id, el);\n        else nodeEls.current.delete(id);\n      },\n      [],\n    );\n\n    // Re-subscribe when the node count changes so new cards get observed.\n    // biome-ignore lint/correctness/useExhaustiveDependencies: intentional re-observe on count change\n    React.useEffect(() => {\n      const measure = () => {\n        setSizes((prev) => {\n          let changed = false;\n          const next = { ...prev };\n          for (const [id, el] of nodeEls.current) {\n            const w = el.offsetWidth;\n            const h = el.offsetHeight;\n            if (!next[id] || next[id].w !== w || next[id].h !== h) {\n              next[id] = { w, h };\n              changed = true;\n            }\n          }\n          return changed ? next : prev;\n        });\n      };\n      measure();\n      const ro = new ResizeObserver(measure);\n      for (const el of nodeEls.current.values()) ro.observe(el);\n      return () => ro.disconnect();\n    }, [nodes.length]);\n\n    // fitView: scale + center the graph's bounding box so the whole graph is\n    // visible on mount (never scales up past 1:1).\n    React.useEffect(() => {\n      if (!fitView || fitted) return;\n      const el = containerRef.current;\n      if (!el || nodes.length === 0) return;\n      if (Object.keys(sizes).length < nodes.length) return;\n      const rect = el.getBoundingClientRect();\n      if (rect.width === 0 || rect.height === 0) return;\n      let minX = Number.POSITIVE_INFINITY;\n      let minY = Number.POSITIVE_INFINITY;\n      let maxX = Number.NEGATIVE_INFINITY;\n      let maxY = Number.NEGATIVE_INFINITY;\n      for (const n of nodes) {\n        const p = posOf(n.id);\n        const s = sizeOf(n.id);\n        minX = Math.min(minX, p.x - s.w / 2);\n        minY = Math.min(minY, p.y - s.h / 2);\n        maxX = Math.max(maxX, p.x + s.w / 2);\n        maxY = Math.max(maxY, p.y + s.h / 2);\n      }\n      const pad = 32;\n      const bboxW = Math.max(1, maxX - minX);\n      const bboxH = Math.max(1, maxY - minY);\n      const s = Math.min(\n        1,\n        (rect.width - pad) / bboxW,\n        (rect.height - pad) / bboxH,\n      );\n      setScale(s);\n      setPan({\n        x: rect.width / 2 - (s * (minX + maxX)) / 2,\n        y: rect.height / 2 - (s * (minY + maxY)) / 2,\n      });\n      setFitted(true);\n    }, [fitView, fitted, nodes, sizes, posOf, sizeOf]);\n\n    // Manual pointer drag — nodes take priority (stopPropagation), backdrop pans.\n    const drag = React.useRef<{\n      kind: \"node\" | \"pan\";\n      id?: string;\n      px: number;\n      py: number;\n      ox: number;\n      oy: number;\n    } | null>(null);\n\n    const onNodePointerDown = (e: React.PointerEvent, id: string) => {\n      if (!draggable) return;\n      e.stopPropagation();\n      e.currentTarget.setPointerCapture(e.pointerId);\n      const p = posOf(id);\n      drag.current = {\n        kind: \"node\",\n        id,\n        px: e.clientX,\n        py: e.clientY,\n        ox: p.x,\n        oy: p.y,\n      };\n    };\n\n    const onNodePointerMove = (e: React.PointerEvent) => {\n      const d = drag.current;\n      if (d?.kind !== \"node\" || !d.id) return;\n      // Screen delta → canvas delta (undo the fit scale).\n      const nx = d.ox + (e.clientX - d.px) / scale;\n      const ny = d.oy + (e.clientY - d.py) / scale;\n      setPositions((prev) => ({ ...prev, [d.id as string]: { x: nx, y: ny } }));\n    };\n\n    const onBackdropPointerDown = (e: React.PointerEvent) => {\n      if (!pannable) return;\n      e.currentTarget.setPointerCapture(e.pointerId);\n      drag.current = {\n        kind: \"pan\",\n        px: e.clientX,\n        py: e.clientY,\n        ox: pan.x,\n        oy: pan.y,\n      };\n    };\n\n    const onBackdropPointerMove = (e: React.PointerEvent) => {\n      const d = drag.current;\n      if (d?.kind !== \"pan\") return;\n      setPan({ x: d.ox + (e.clientX - d.px), y: d.oy + (e.clientY - d.py) });\n    };\n\n    const endDrag = (e: React.PointerEvent) => {\n      if (drag.current) e.currentTarget.releasePointerCapture?.(e.pointerId);\n      drag.current = null;\n    };\n\n    // ── Auto-play choreography ────────────────────────────────────────────\n    // Graph shape: which edges leave a node, which node an edge targets, and\n    // which nodes are roots (no incoming edge — where the sequence begins).\n    const graph = React.useMemo(() => {\n      const outgoing = new Map<string, string[]>();\n      const target = new Map<string, string>();\n      const hasIncoming = new Set<string>();\n      for (const n of nodes) outgoing.set(n.id, []);\n      for (const e of edges) {\n        outgoing.get(e.from)?.push(e.id);\n        target.set(e.id, e.to);\n        hasIncoming.add(e.to);\n      }\n      const roots = nodes\n        .filter((n) => !hasIncoming.has(n.id))\n        .map((n) => n.id);\n      return { outgoing, target, roots };\n    }, [nodes, edges]);\n\n    const [seq, dispatch] = React.useReducer(seqReducer, EMPTY_SEQ);\n\n    // Start / restart the sequence when autoPlay turns on or the graph changes.\n    React.useEffect(() => {\n      if (!autoPlay) return;\n      dispatch({ type: \"reset\", roots: graph.roots });\n    }, [autoPlay, graph]);\n\n    const handleTraceComplete = React.useCallback(\n      (id: string) => {\n        onNodeActivate?.(id);\n        if (!autoPlay) return;\n        dispatch({\n          type: \"traceDone\",\n          id,\n          outEdges: graph.outgoing.get(id) ?? [],\n        });\n      },\n      [autoPlay, graph, onNodeActivate],\n    );\n\n    const handleArrive = React.useCallback(\n      (edgeId: string) => {\n        onEdgeArrive?.(edgeId);\n        if (!autoPlay) return;\n        const to = graph.target.get(edgeId);\n        if (to) dispatch({ type: \"arrive\", edgeId, target: to });\n      },\n      [autoPlay, graph, onEdgeArrive],\n    );\n\n    // Loop: once every node is lit and nothing is in flight, replay after a beat.\n    React.useEffect(() => {\n      if (!autoPlay || !continuous || nodes.length === 0) return;\n      const settled =\n        seq.lit.size === nodes.length &&\n        seq.flowing.size === 0 &&\n        seq.tracing.size === 0;\n      if (!settled) return;\n      const t = setTimeout(\n        () => dispatch({ type: \"reset\", roots: graph.roots }),\n        1200,\n      );\n      return () => clearTimeout(t);\n    }, [autoPlay, continuous, nodes.length, seq, graph]);\n\n    const statusOf = (n: AgentFlowNode): AgentNodeStatus => {\n      if (!autoPlay) return n.status ?? \"idle\";\n      if (seq.lit.has(n.id)) return \"done\";\n      if (seq.tracing.has(n.id)) return \"running\";\n      return \"idle\";\n    };\n\n    const flowEdges: AgentFlowEdge[] = autoPlay\n      ? edges.map((e) => ({\n          ...e,\n          animated: seq.flowing.has(e.id),\n          loop: false,\n        }))\n      : edges;\n\n    return (\n      // biome-ignore lint/a11y/useSemanticElements: a pan/zoom canvas group has no single semantic element\n      <div\n        ref={(node) => {\n          containerRef.current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) ref.current = node;\n        }}\n        data-slot=\"agent-flow\"\n        role=\"group\"\n        aria-label={ariaLabel ?? \"Agent workflow\"}\n        className={`relative overflow-hidden rounded-2xl border border-border bg-background [background-image:radial-gradient(var(--border)_1px,transparent_1px)] [background-size:20px_20px] ${\n          pannable ? \"cursor-grab active:cursor-grabbing\" : \"\"\n        } ${className ?? \"\"}`}\n        onPointerDown={onBackdropPointerDown}\n        onPointerMove={onBackdropPointerMove}\n        onPointerUp={endDrag}\n        onPointerCancel={endDrag}\n        {...props}\n      >\n        <div\n          className=\"absolute left-0 top-0 h-full w-full\"\n          style={{\n            transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})`,\n            transformOrigin: \"0 0\",\n            // Stay hidden until fitView has framed the graph, so it fades into\n            // place instead of snapping from the unscaled corner.\n            opacity: !fitView || fitted ? 1 : 0,\n            transition: \"opacity 300ms cubic-bezier(0.22,1,0.36,1)\",\n          }}\n        >\n          <Edges\n            edges={flowEdges}\n            posOf={posOf}\n            sizeOf={sizeOf}\n            flowDuration={flowDuration}\n            flowSpeed={flowSpeed}\n            matchSpeed={autoPlay}\n            reduce={reduce}\n            onArrive={handleArrive}\n          />\n          {nodes.map((n, i) => {\n            const p = posOf(n.id);\n            return (\n              <NodeCard\n                key={n.id}\n                cardRef={registerNode(n.id)}\n                node={n}\n                status={statusOf(n)}\n                size={sizeOf(n.id)}\n                index={i}\n                x={p.x}\n                y={p.y}\n                reduce={reduce}\n                draggable={draggable}\n                flowSpeed={flowSpeed}\n                onTraceComplete={handleTraceComplete}\n                onPointerDown={(e) => onNodePointerDown(e, n.id)}\n                onPointerMove={onNodePointerMove}\n                onPointerUp={endDrag}\n              />\n            );\n          })}\n        </div>\n      </div>\n    );\n  },\n);\nAgentFlow.displayName = \"AgentFlow\";\n\nfunction NodeCard({\n  node,\n  status,\n  size,\n  index,\n  x,\n  y,\n  reduce,\n  draggable,\n  flowSpeed,\n  cardRef,\n  onTraceComplete,\n  onPointerDown,\n  onPointerMove,\n  onPointerUp,\n}: {\n  node: AgentFlowNode;\n  status: AgentNodeStatus;\n  size: Size;\n  index: number;\n  x: number;\n  y: number;\n  reduce: boolean | null;\n  draggable: boolean;\n  flowSpeed: number;\n  cardRef: (el: HTMLElement | null) => void;\n  onTraceComplete: (id: string) => void;\n  onPointerDown: (e: React.PointerEvent) => void;\n  onPointerMove: (e: React.PointerEvent) => void;\n  onPointerUp: (e: React.PointerEvent) => void;\n}) {\n  const glowId = `agent-flow-node-${React.useId()}`;\n  const active = status === \"running\" || status === \"done\";\n  const [top, bottom] = borderTracePaths(size.w, size.h);\n  // Border trace duration derives from this card's outline length so the front\n  // moves at exactly `flowSpeed`, matching the beams that continue from it.\n  const traceDuration = borderOutlineLength(size.w, size.h) / flowSpeed;\n  // Linear, not ease-out: constant speed is what makes the border→line handoff\n  // seamless (an ease-out front would decelerate just as the beam starts).\n  const traceT = reduce\n    ? { duration: 0 }\n    : { duration: traceDuration, ease: EASE_LINEAR };\n\n  // The icon lights when the border trace is halfway drawn (not at the end).\n  const [iconLit, setIconLit] = React.useState(status === \"done\");\n  React.useEffect(() => {\n    if (status !== \"running\") {\n      setIconLit(status === \"done\");\n      return;\n    }\n    if (reduce) {\n      setIconLit(true);\n      return;\n    }\n    const t = setTimeout(() => setIconLit(true), (traceDuration * 1000) / 2);\n    return () => clearTimeout(t);\n  }, [status, reduce, traceDuration]);\n\n  // Report the border as complete from a timer (reliable, unlike the SVG path's\n  // animation-complete event) so the outgoing edge always fires — including the\n  // graph's last hop.\n  React.useEffect(() => {\n    if (status !== \"running\") return;\n    if (reduce) {\n      onTraceComplete(node.id);\n      return;\n    }\n    const t = setTimeout(() => onTraceComplete(node.id), traceDuration * 1000);\n    return () => clearTimeout(t);\n  }, [status, reduce, traceDuration, node.id, onTraceComplete]);\n\n  return (\n    <motion.div\n      ref={cardRef}\n      data-status={status}\n      initial={reduce ? false : { opacity: 0, filter: \"blur(8px)\" }}\n      animate={{ opacity: 1, filter: \"blur(0px)\" }}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : { delay: index * 0.05, duration: 0.4, ease: EASE_OUT }\n      }\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerUp}\n      onPointerCancel={onPointerUp}\n      className={`absolute z-raised flex w-max max-w-[15rem] -translate-x-1/2 -translate-y-1/2 select-none items-center gap-3 rounded-xl border bg-background/80 p-3 shadow-sm backdrop-blur [transition:border-color_400ms_cubic-bezier(0.22,1,0.36,1),box-shadow_400ms_cubic-bezier(0.22,1,0.36,1),background-color_400ms_cubic-bezier(0.22,1,0.36,1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${\n        STATUS_CARD[status]\n      } ${draggable ? \"cursor-grab touch-none active:cursor-grabbing\" : \"\"}`}\n      style={{ left: x, top: y }}\n    >\n      {/* Border trace — grows from the left-edge centre out to both sides and\n          persists as the node's lit border once drawn. */}\n      {active && size.w > 1 ? (\n        <svg\n          fill=\"none\"\n          aria-hidden=\"true\"\n          width={size.w}\n          height={size.h}\n          viewBox={`0 0 ${size.w} ${size.h}`}\n          className=\"pointer-events-none absolute inset-0 overflow-visible\"\n        >\n          <defs>\n            <filter id={glowId} x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\">\n              <feGaussianBlur stdDeviation=\"2.5\" />\n            </filter>\n          </defs>\n          {[top, bottom].map((d) => (\n            <g key={d}>\n              <motion.path\n                d={d}\n                stroke=\"var(--primary)\"\n                strokeWidth={3}\n                strokeLinecap=\"round\"\n                strokeOpacity={0.22}\n                filter={`url(#${glowId})`}\n                initial={{ pathLength: reduce ? 1 : 0 }}\n                animate={{ pathLength: 1 }}\n                transition={traceT}\n              />\n              <motion.path\n                d={d}\n                stroke=\"var(--primary)\"\n                strokeWidth={1.5}\n                strokeLinecap=\"round\"\n                strokeOpacity={0.7}\n                initial={{ pathLength: reduce ? 1 : 0 }}\n                animate={{ pathLength: 1 }}\n                transition={traceT}\n              />\n            </g>\n          ))}\n        </svg>\n      ) : null}\n      <StatusChip\n        status={status}\n        icon={node.icon}\n        lit={iconLit}\n        reduce={reduce}\n      />\n      <span className=\"min-w-0\">\n        <span className=\"block truncate text-sm font-medium text-foreground\">\n          {node.label}\n        </span>\n        {node.sublabel ? (\n          <span className=\"block truncate text-xs text-muted-foreground\">\n            {node.sublabel}\n          </span>\n        ) : null}\n      </span>\n    </motion.div>\n  );\n}\n\nfunction StatusChip({\n  status,\n  icon,\n  lit,\n  reduce,\n}: {\n  status: AgentNodeStatus;\n  icon?: React.ReactNode;\n  /** The icon is lit (drives the pop + solid chip); set at half the trace. */\n  lit: boolean;\n  reduce: boolean | null;\n}) {\n  const appearance: AgentNodeStatus =\n    status === \"error\" ? \"error\" : lit ? \"done\" : \"idle\";\n  return (\n    <span\n      className={`relative flex size-8 shrink-0 items-center justify-center rounded-lg border [transition:background-color_300ms_cubic-bezier(0.22,1,0.36,1),border-color_300ms_cubic-bezier(0.22,1,0.36,1),color_300ms_cubic-bezier(0.22,1,0.36,1)] ${STATUS_CHIP[appearance]}`}\n    >\n      {/* Keyed by appearance so the glyph pops in when the icon lights. */}\n      <motion.span\n        key={appearance}\n        initial={\n          reduce ? false : { scale: 0.4, opacity: 0, filter: \"blur(3px)\" }\n        }\n        animate={{ scale: 1, opacity: 1, filter: \"blur(0px)\" }}\n        transition={reduce ? { duration: 0 } : SPRING_SNAPPY}\n        className=\"flex size-4 items-center justify-center [&>svg]:size-4\"\n      >\n        {icon ? (\n          icon\n        ) : appearance === \"done\" ? (\n          <CheckIcon className=\"size-4\" />\n        ) : appearance === \"error\" ? (\n          <CloseIcon className=\"size-4\" />\n        ) : (\n          <span className=\"size-1.5 rounded-full bg-current\" />\n        )}\n      </motion.span>\n      {status === \"running\" && !lit && !reduce ? (\n        <span className=\"absolute inset-0 animate-ping rounded-lg border border-primary/40 motion-reduce:animate-none\" />\n      ) : null}\n    </span>\n  );\n}\n\nfunction Edges({\n  edges,\n  posOf,\n  sizeOf,\n  flowDuration,\n  flowSpeed,\n  matchSpeed,\n  reduce,\n  onArrive,\n}: {\n  edges: AgentFlowEdge[];\n  posOf: (id: string) => Point;\n  sizeOf: (id: string) => Size;\n  flowDuration: number;\n  flowSpeed: number;\n  /** Travel each edge at `flowSpeed` (px/s) instead of a fixed duration. */\n  matchSpeed: boolean;\n  reduce: boolean | null;\n  onArrive?: (edgeId: string) => void;\n}) {\n  const glowId = `agent-flow-glow-${React.useId()}`;\n  return (\n    <svg\n      fill=\"none\"\n      aria-hidden=\"true\"\n      role=\"presentation\"\n      className=\"pointer-events-none absolute left-0 top-0 h-full w-full overflow-visible [transform:translateZ(0)]\"\n    >\n      <defs>\n        {/* Soft bloom for the travelling packet — the \"glowing trail\". */}\n        <filter id={glowId} x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\">\n          <feGaussianBlur stdDeviation=\"3\" />\n        </filter>\n      </defs>\n      {edges.map((edge) => {\n        const from = posOf(edge.from);\n        const to = posOf(edge.to);\n        const fs = sizeOf(edge.from);\n        const ts = sizeOf(edge.to);\n        const startX = from.x + fs.w / 2;\n        const startY = from.y;\n        const endX = to.x - ts.w / 2;\n        const endY = to.y;\n        const controlX = (startX + endX) / 2;\n        const controlY = (startY + endY) / 2 - (edge.curvature ?? 0);\n        const d = `M ${startX},${startY} Q ${controlX},${controlY} ${endX},${endY}`;\n        // Continuous flow: the packet covers this edge's length at the shared\n        // `flowSpeed` (px/s) — the same pace the borders trace — so the light\n        // never speeds up or slows at the card→line seam. Curved edges use the\n        // control point for a real (not chord) length estimate.\n        const chord = Math.hypot(endX - startX, endY - startY);\n        const curveLen =\n          Math.hypot(controlX - startX, controlY - startY) +\n          Math.hypot(endX - controlX, endY - controlY);\n        const len = (chord + curveLen) / 2;\n        const edgeDuration = matchSpeed\n          ? Math.max(0.2, len / flowSpeed)\n          : flowDuration;\n        return (\n          <Edge\n            key={edge.id}\n            id={edge.id}\n            d={d}\n            startX={startX}\n            startY={startY}\n            endX={endX}\n            endY={endY}\n            animated={edge.animated ?? true}\n            loop={edge.loop ?? true}\n            persist={edge.persist ?? false}\n            flowDuration={edgeDuration}\n            reduce={reduce}\n            glowId={glowId}\n            onArrive={onArrive}\n          />\n        );\n      })}\n    </svg>\n  );\n}\n\n// Memoized so a sibling edge landing (which re-renders the parent) never\n// restarts an edge that is still mid-flight — that caused packets to replay.\nconst Edge = React.memo(function Edge({\n  id,\n  d,\n  startX,\n  startY,\n  endX,\n  endY,\n  animated,\n  loop,\n  persist,\n  flowDuration,\n  reduce,\n  glowId,\n  onArrive,\n}: {\n  id: string;\n  d: string;\n  startX: number;\n  startY: number;\n  endX: number;\n  endY: number;\n  animated: boolean;\n  loop: boolean;\n  persist: boolean;\n  flowDuration: number;\n  reduce: boolean | null;\n  glowId: string;\n  onArrive?: (edgeId: string) => void;\n}) {\n  const gid = `agent-flow-${React.useId()}-${id}`;\n  const playing = animated && !reduce;\n\n  // Report arrival from a timer rather than the gradient's animation-complete\n  // event: it always fires (the SVG-gradient complete event can be missed) and\n  // lands just before the packet reaches the node, so the next border starts\n  // the instant the line finishes — one continuous flow, no gap.\n  React.useEffect(() => {\n    if (!animated || loop) return;\n    if (reduce) {\n      onArrive?.(id);\n      return;\n    }\n    const t = setTimeout(() => onArrive?.(id), flowDuration * 1000 * 0.98);\n    return () => clearTimeout(t);\n  }, [animated, loop, reduce, flowDuration, id, onArrive]);\n\n  // Persisted edges stay lit once their packet has passed. Track completion so\n  // the drawn-on line holds after `animated` drops back to false; a fresh flow\n  // (continuous replay) clears it so the line re-draws from empty.\n  const [lit, setLit] = React.useState(false);\n  const wasAnimated = React.useRef(false);\n  React.useEffect(() => {\n    if (!persist) return;\n    if (animated && !wasAnimated.current) setLit(false);\n    wasAnimated.current = animated;\n    if (!animated) return;\n    if (reduce) {\n      setLit(true);\n      return;\n    }\n    // Fire just before arrival (0.98·flowDuration) un-animates the edge, so the\n    // persisted trail is already latched lit when `playing` drops to false and\n    // the `<g>` would otherwise unmount.\n    const t = setTimeout(() => setLit(true), flowDuration * 1000 * 0.95);\n    return () => clearTimeout(t);\n  }, [persist, animated, reduce, flowDuration]);\n\n  // Gradient axis in user space, along THIS edge's start→end chord. Absolute\n  // coordinates (not %) with userSpaceOnUse so the sweep travels every edge\n  // regardless of its position on the shared canvas AND works on flat\n  // horizontal/vertical edges, whose zero-area bounding box makes an\n  // objectBoundingBox gradient fail to render entirely.\n  const dx = endX - startX;\n  const dy = endY - startY;\n  const at = (f: number) => ({ x: startX + f * dx, y: startY + f * dy });\n  const anim = (a: number, b: number) => ({\n    x1: [at(a).x, at(b).x],\n    y1: [at(a).y, at(b).y],\n    x2: [at(a - 0.1).x, at(b - 0.1).x],\n    y2: [at(a - 0.1).y, at(b - 0.1).y],\n  });\n  const parked = { x1: at(1.2).x, y1: at(1.2).y, x2: at(1.1).x, y2: at(1.1).y };\n\n  const drawT = reduce\n    ? { duration: 0 }\n    : { duration: flowDuration, ease: \"linear\" as const };\n\n  return (\n    <>\n      <path\n        d={d}\n        stroke=\"var(--border)\"\n        strokeWidth={2}\n        strokeOpacity={0.5}\n        strokeLinecap=\"round\"\n      />\n      {/* Persisted trail — a primary stroke that draws on (pathLength) in sync\n          with the travelling packet and stays lit, mirroring a card's traced\n          border. Only for `persist` edges; kept mounted by `lit` after the flow\n          so a landed edge reads as an established connection. */}\n      {persist && (playing || lit) ? (\n        <g>\n          <motion.path\n            d={d}\n            stroke=\"var(--primary)\"\n            strokeWidth={4}\n            strokeLinecap=\"round\"\n            strokeOpacity={0.25}\n            filter={`url(#${glowId})`}\n            initial={{ pathLength: reduce || lit ? 1 : 0 }}\n            animate={{ pathLength: 1 }}\n            transition={lit ? { duration: 0 } : drawT}\n          />\n          <motion.path\n            d={d}\n            stroke=\"var(--primary)\"\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n            strokeOpacity={0.85}\n            initial={{ pathLength: reduce || lit ? 1 : 0 }}\n            animate={{ pathLength: 1 }}\n            transition={lit ? { duration: 0 } : drawT}\n          />\n        </g>\n      ) : null}\n      {/* Packet — a bright head sweeping the edge. On persist edges it rides the\n          leading tip of the drawing trail like a comet; on transient edges it is\n          the whole animation, dropping back to the resting path when done. A\n          blurred copy trails behind the sharp stroke for a soft glow. */}\n      {playing ? (\n        <motion.g\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={{ duration: 0.2, ease: EASE_OUT }}\n        >\n          <path\n            d={d}\n            stroke={`url(#${gid})`}\n            strokeWidth={4}\n            strokeLinecap=\"round\"\n            strokeOpacity={0.55}\n            filter={`url(#${glowId})`}\n          />\n          <path\n            d={d}\n            stroke={`url(#${gid})`}\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n          />\n        </motion.g>\n      ) : null}\n      <defs>\n        <motion.linearGradient\n          id={gid}\n          gradientUnits=\"userSpaceOnUse\"\n          initial={parked}\n          animate={\n            playing\n              ? // A loop overshoots the end so the streak exits smoothly; a\n                // single packet lands its leading edge exactly at the node so\n                // completion coincides with the visual arrival (the next node\n                // starts the moment the line finishes).\n                loop\n                ? anim(0, 1.1)\n                : anim(0, 1)\n              : // Park the packet just past the end (invisible) when idle/landed,\n                // so a finished edge never flashes a full line before it unmounts.\n                parked\n          }\n          transition={\n            playing\n              ? {\n                  duration: flowDuration,\n                  repeat: loop ? Number.POSITIVE_INFINITY : 0,\n                  // Linear so the packet keeps a constant speed and finishes the\n                  // instant its leading edge reaches the node — no easing lag\n                  // between the line ending and the next border starting.\n                  ease: \"linear\",\n                }\n              : { duration: 0 }\n          }\n        >\n          <stop stopColor=\"var(--primary)\" stopOpacity=\"0\" />\n          <stop stopColor=\"var(--primary)\" />\n          <stop\n            offset=\"32.5%\"\n            stopColor=\"color-mix(in oklch, var(--primary) 40%, transparent)\"\n          />\n          <stop\n            offset=\"100%\"\n            stopColor=\"color-mix(in oklch, var(--primary) 40%, transparent)\"\n            stopOpacity=\"0\"\n          />\n        </motion.linearGradient>\n      </defs>\n    </>\n  );\n});\n\ntype IconProps = { className?: string };\nfunction CheckIcon({ className }: IconProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={3}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={className}\n      aria-hidden=\"true\"\n    >\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  );\n}\nfunction CloseIcon({ className }: IconProps) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={3}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={className}\n      aria-hidden=\"true\"\n    >\n      <path d=\"M18 6 6 18M6 6l12 12\" />\n    </svg>\n  );\n}\n\nexport { AgentFlow };\n",
      "type": "registry:ui",
      "target": "components/godui/agent-flow.tsx"
    }
  ],
  "type": "registry:ui"
}
