{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-flow-core",
  "title": "Flow core primitive",
  "description": "Node/edge graphs for CI pipelines, agent runs, payment routes and approval flows: a typed graph on thinking-core's StepStatus, a small-DAG auto layout (ranks, crossing reduction, curve or step edges), fitView, status propagation with stepFlow, and edges drawn on pathLength with useEdgeProgress and FlowEdgePath.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-thinking-core.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/flow-core/edge-progress.tsx",
      "content": "\"use client\";\n\nimport { animate, motion, useMotionValue, type MotionValue } from \"motion/react\";\nimport { useEffect } from \"react\";\nimport { ease, useSafeReducedMotion } from \"../../lib/motion\";\nimport type { StepStatus } from \"../thinking-core/types\";\nimport { edgeProgressTarget } from \"./status\";\n\nexport type EdgeProgressOptions = {\n  /** Seconds to draw an edge. Default 0.5: a hand-off, read at a glance. */\n  duration?: number;\n  /** Seconds before drawing starts, e.g. after the source node's check has drawn. Default 0. */\n  delay?: number;\n  /** Override the reduced-motion read (it is read after hydration by default). */\n  reduced?: boolean;\n};\n\n/**\n * An edge's `pathLength` (0–1) as a motion value that follows its status: it draws from the\n * source to the target when the edge becomes active or complete, and rewinds faster (≤ 0.75 ×\n * the draw) when a retry resets it. Interruptible: it always continues from what's on screen.\n * Reduced motion jumps. A complete edge on mount starts drawn, so a finished run doesn't replay.\n */\nexport function useEdgeProgress(status: StepStatus, { duration = 0.5, delay = 0, reduced: reducedProp }: EdgeProgressOptions = {}): MotionValue<number> {\n  const reducedRead = useSafeReducedMotion();\n  const reduced = reducedProp ?? reducedRead;\n  const target = edgeProgressTarget(status);\n  const progress = useMotionValue(status === \"complete\" ? 1 : 0);\n  useEffect(() => {\n    if (progress.get() === target) return;\n    if (reduced) {\n      progress.jump(target);\n      return;\n    }\n    const rewinding = target < progress.get();\n    const controls = animate(progress, target, {\n      duration: rewinding ? duration * 0.6 : duration,\n      delay: rewinding ? 0 : delay,\n      ease: rewinding ? ease.exit : ease.inOut,\n    });\n    return () => controls.stop();\n  }, [target, reduced, duration, delay, progress]);\n  return progress;\n}\n\nexport type FlowEdgePathProps = EdgeProgressOptions & {\n  /** SVG path data, e.g. `PlacedEdge.path`. */\n  d: string;\n  status: StepStatus;\n  /** Classes for the drawn stroke. Default `stroke-text-secondary`; tint it per status. */\n  className?: string;\n  /** Classes for the undrawn track. Default `stroke-line`; skipped edges are dashed. */\n  trackClassName?: string;\n  strokeWidth?: number;\n};\n\n/**\n * The one piece of UI flow-core ships: an edge as a track plus a stroke drawn along it by\n * `useEdgeProgress`. Everything else (nodes, labels, colours per status) is the builder's.\n */\nexport function FlowEdgePath({ d, status, className = \"stroke-text-secondary\", trackClassName = \"stroke-line\", strokeWidth = 1.5, ...options }: FlowEdgePathProps) {\n  const progress = useEdgeProgress(status, options);\n  return (\n    <g fill=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\" data-status={status}>\n      <path d={d} className={trackClassName} strokeWidth={strokeWidth} strokeDasharray={status === \"skipped\" ? \"2 4\" : undefined} />\n      <motion.path d={d} className={className} strokeWidth={strokeWidth} style={{ pathLength: progress, opacity: status === \"skipped\" ? 0 : 1 }} />\n    </g>\n  );\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/flow-core/edge-progress.tsx"
    },
    {
      "path": "src/registry/eased/_primitives/flow-core/flow-core.ts",
      "content": "/**\n * flow-core: node/edge graphs for CI pipelines, agent runs, payment routes, org charts and\n * approval flows. A typed graph model whose statuses are thinking-core's `StepStatus`, a small\n * DAG auto layout, status propagation, and edges drawn on `pathLength`.\n * Import from here; the files behind it may move.\n */\nexport { edgeId, type FlowEdge, type FlowGraph, type FlowNode, type StepStatus } from \"./types\";\nexport {\n  curvePath,\n  fitView,\n  layoutFlow,\n  stepPath,\n  type FlowDirection,\n  type FlowLayout,\n  type FlowLayoutOptions,\n  type PlacedEdge,\n  type PlacedNode,\n} from \"./layout\";\nexport {\n  descendants,\n  edgeProgressTarget,\n  edgeStatus,\n  nodeMap,\n  predecessors,\n  readyNodes,\n  resetFlow,\n  resolveEdgeStatus,\n  setNodeStatus,\n  stepFlow,\n  summarizeFlow,\n} from \"./status\";\nexport { FlowEdgePath, useEdgeProgress, type EdgeProgressOptions, type FlowEdgePathProps } from \"./edge-progress\";\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/flow-core/flow-core.ts"
    },
    {
      "path": "src/registry/eased/_primitives/flow-core/layout.ts",
      "content": "import { edgeId, type FlowEdge, type FlowGraph, type FlowNode } from \"./types\";\n\nexport type FlowDirection = \"LR\" | \"TB\";\n\nexport type FlowLayoutOptions = {\n  /** Left to right (pipelines, payment routes) or top to bottom (org charts, approval chains). Default `LR`. */\n  direction?: FlowDirection;\n  /** Default node size; a node's own `width`/`height` wins. Default 152 × 40. */\n  nodeWidth?: number;\n  nodeHeight?: number;\n  /** Space between ranks (columns in LR). Default 40. */\n  rankGap?: number;\n  /** Space between siblings in a rank. Default 12. */\n  nodeGap?: number;\n  /** Space around the whole graph. Default 0. */\n  padding?: number;\n  /** `curve` is a smooth S-bezier; `step` is orthogonal with rounded corners (GitHub Actions). Default `curve`. */\n  edgeShape?: \"curve\" | \"step\";\n  /** Corner radius of `step` edges. Default 8. */\n  cornerRadius?: number;\n};\n\nexport type PlacedNode<D = unknown> = {\n  node: FlowNode<D>;\n  id: string;\n  /** Top-left corner and size, in layout px. */\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  /** Column (LR) or row (TB) index, from 0 at the sources. */\n  rank: number;\n  /** Position inside its rank, after crossing reduction. */\n  order: number;\n};\n\nexport type PlacedEdge = {\n  edge: FlowEdge;\n  id: string;\n  from: string;\n  to: string;\n  /** SVG path data. Draw it with `pathLength` progress (`FlowEdgePath`, `useEdgeProgress`). */\n  path: string;\n  /** Start (source port) and end (target port). */\n  start: { x: number; y: number };\n  end: { x: number; y: number };\n  /** Points against the ranking: a cycle was broken here. Drawn, but not used for layout. */\n  back: boolean;\n};\n\nexport type FlowLayout<D = unknown> = {\n  nodes: PlacedNode<D>[];\n  edges: PlacedEdge[];\n  /** Node ids per rank, in order. */\n  ranks: string[][];\n  width: number;\n  height: number;\n  byId: Map<string, PlacedNode<D>>;\n};\n\n/**\n * Lays out a small DAG (up to a few dozen nodes): longest-path ranks, barycentre sweeps to\n * reduce crossings, ranks centred on each other, edges from the trailing port of a node to the\n * leading port of the next. Pure and deterministic (input order breaks ties), so call it in a\n * `useMemo` and it gives the same picture on the server and the client. Cycles are tolerated:\n * the closing edge is marked `back` and skipped for ranking.\n *\n *   const layout = useMemo(() => layoutFlow(pipeline, { edgeShape: \"step\" }), [pipeline]);\n */\nexport function layoutFlow<D>(graph: FlowGraph<D>, options: FlowLayoutOptions = {}): FlowLayout<D> {\n  const {\n    direction = \"LR\",\n    nodeWidth = 152,\n    nodeHeight = 40,\n    rankGap = 40,\n    nodeGap = 12,\n    padding = 0,\n    edgeShape = \"curve\",\n    cornerRadius = 8,\n  } = options;\n  const lr = direction === \"LR\";\n  const nodes = graph.nodes;\n  const index = new Map(nodes.map((n, i) => [n.id, i]));\n  const edges = graph.edges.filter((e) => index.has(e.from) && index.has(e.to) && e.from !== e.to);\n\n  // 1. Break cycles: DFS in input order; an edge to a node on the stack is a back edge.\n  const out: number[][] = nodes.map(() => []);\n  edges.forEach((e, i) => out[index.get(e.from)!].push(i));\n  const back = new Set<number>();\n  const state = new Uint8Array(nodes.length); // 0 new, 1 on stack, 2 done\n  const visit = (v: number) => {\n    state[v] = 1;\n    for (const ei of out[v]) {\n      const w = index.get(edges[ei].to)!;\n      if (state[w] === 1) back.add(ei);\n      else if (state[w] === 0) visit(w);\n    }\n    state[v] = 2;\n  };\n  nodes.forEach((_, v) => state[v] === 0 && visit(v));\n  const forward = edges.map((e, i) => ({ e, i, from: index.get(e.from)!, to: index.get(e.to)! })).filter((x) => !back.has(x.i));\n\n  // 2. Longest-path ranks (Kahn's order).\n  const preds: number[][] = nodes.map(() => []);\n  const succs: number[][] = nodes.map(() => []);\n  for (const f of forward) {\n    preds[f.to].push(f.from);\n    succs[f.from].push(f.to);\n  }\n  const rank = new Array<number>(nodes.length).fill(0);\n  const indeg = preds.map((p) => p.length);\n  const queue = nodes.map((_, i) => i).filter((i) => indeg[i] === 0);\n  for (let q = 0; q < queue.length; q++) {\n    const v = queue[q];\n    for (const w of succs[v]) {\n      rank[w] = Math.max(rank[w], rank[v] + 1);\n      if (--indeg[w] === 0) queue.push(w);\n    }\n  }\n  // Pull sources forward to sit just before their first consumer (a lone \"lint\" next to \"test\", not at column 0).\n  for (let i = queue.length - 1; i >= 0; i--) {\n    const v = queue[i];\n    if (preds[v].length === 0 && succs[v].length) rank[v] = Math.min(...succs[v].map((w) => rank[w])) - 1;\n  }\n\n  const rankCount = nodes.length ? Math.max(...rank) + 1 : 0;\n  const ranks: number[][] = Array.from({ length: rankCount }, () => []);\n  nodes.forEach((_, v) => ranks[rank[v]].push(v));\n\n  // 3. Crossing reduction: barycentre sweeps down then up, ties kept in input order.\n  const pos = new Array<number>(nodes.length);\n  const setPos = () => ranks.forEach((r) => r.forEach((v, i) => (pos[v] = i)));\n  setPos();\n  const sweep = (r: number[], neighbours: number[][]) => {\n    const bary = new Map<number, number>();\n    for (const v of r) {\n      const ns = neighbours[v];\n      bary.set(v, ns.length ? ns.reduce((s, w) => s + pos[w], 0) / ns.length : pos[v]);\n    }\n    r.sort((a, b) => bary.get(a)! - bary.get(b)! || pos[a] - pos[b]);\n    r.forEach((v, i) => (pos[v] = i));\n  };\n  for (let iter = 0; iter < 4; iter++) {\n    for (let r = 1; r < rankCount; r++) sweep(ranks[r], preds);\n    for (let r = rankCount - 2; r >= 0; r--) sweep(ranks[r], succs);\n  }\n\n  // 4. Coordinates. \"Along\" is the rank axis (x in LR), \"across\" stacks a rank's nodes.\n  const size = (v: number) => ({ w: nodes[v].width ?? nodeWidth, h: nodes[v].height ?? nodeHeight });\n  const alongSize = (v: number) => (lr ? size(v).w : size(v).h);\n  const acrossSize = (v: number) => (lr ? size(v).h : size(v).w);\n  const rankDepth = ranks.map((r) => Math.max(0, ...r.map(alongSize)));\n  const rankSpan = ranks.map((r) => r.reduce((s, v) => s + acrossSize(v), 0) + Math.max(0, r.length - 1) * nodeGap);\n  const maxSpan = Math.max(0, ...rankSpan);\n  const placed: PlacedNode<D>[] = new Array(nodes.length);\n  let along = padding;\n  ranks.forEach((r, ri) => {\n    let across = padding + (maxSpan - rankSpan[ri]) / 2;\n    for (const v of r) {\n      const { w, h } = size(v);\n      const a = along + (rankDepth[ri] - alongSize(v)) / 2;\n      placed[v] = { node: nodes[v], id: nodes[v].id, x: lr ? a : across, y: lr ? across : a, width: w, height: h, rank: ri, order: pos[v] };\n      across += acrossSize(v) + nodeGap;\n    }\n    along += rankDepth[ri] + rankGap;\n  });\n  const totalAlong = rankCount ? along - rankGap + padding : 2 * padding;\n  const width = lr ? totalAlong : maxSpan + 2 * padding;\n  const height = lr ? maxSpan + 2 * padding : totalAlong;\n\n  // 5. Edges.\n  const placedEdges: PlacedEdge[] = edges.map((e, i) => {\n    const a = placed[index.get(e.from)!];\n    const b = placed[index.get(e.to)!];\n    const isBack = back.has(i);\n    const start = lr ? { x: a.x + a.width, y: a.y + a.height / 2 } : { x: a.x + a.width / 2, y: a.y + a.height };\n    const end = lr ? { x: b.x, y: b.y + b.height / 2 } : { x: b.x + b.width / 2, y: b.y };\n    return {\n      edge: e,\n      id: edgeId(e),\n      from: e.from,\n      to: e.to,\n      start,\n      end,\n      back: isBack,\n      path: edgeShape === \"step\" ? stepPath(start, end, lr, cornerRadius) : curvePath(start, end, lr),\n    };\n  });\n\n  return {\n    nodes: placed,\n    edges: placedEdges,\n    ranks: ranks.map((r) => r.map((v) => nodes[v].id)),\n    width,\n    height,\n    byId: new Map(placed.map((p) => [p.id, p])),\n  };\n}\n\ntype Pt = { x: number; y: number };\nconst f = (n: number) => Math.round(n * 100) / 100;\n\n/** A smooth S-curve between two ports, tangent to the flow direction at both ends. */\nexport function curvePath(a: Pt, b: Pt, lr = true): string {\n  if (lr) {\n    const dx = Math.max(16, Math.abs(b.x - a.x) / 2);\n    return `M${f(a.x)},${f(a.y)} C${f(a.x + dx)},${f(a.y)} ${f(b.x - dx)},${f(b.y)} ${f(b.x)},${f(b.y)}`;\n  }\n  const dy = Math.max(16, Math.abs(b.y - a.y) / 2);\n  return `M${f(a.x)},${f(a.y)} C${f(a.x)},${f(a.y + dy)} ${f(b.x)},${f(b.y - dy)} ${f(b.x)},${f(b.y)}`;\n}\n\n/** An orthogonal edge that turns once at the midpoint with rounded corners. */\nexport function stepPath(a: Pt, b: Pt, lr = true, radius = 8): string {\n  // Work in \"along/across\" space so one routine serves both directions.\n  const A = lr ? [a.x, a.y] : [a.y, a.x];\n  const B = lr ? [b.x, b.y] : [b.y, b.x];\n  const pt = (al: number, ac: number) => (lr ? `${f(al)},${f(ac)}` : `${f(ac)},${f(al)}`);\n  const d = B[1] - A[1];\n  if (Math.abs(d) < 0.5) return `M${pt(A[0], A[1])} L${pt(B[0], B[1])}`;\n  const mid = (A[0] + B[0]) / 2;\n  const s = Math.sign(d);\n  const r = Math.max(0, Math.min(radius, Math.abs(d) / 2, Math.abs(B[0] - A[0]) / 2));\n  return [\n    `M${pt(A[0], A[1])}`,\n    `L${pt(mid - r, A[1])}`,\n    `Q${pt(mid, A[1])} ${pt(mid, A[1] + s * r)}`,\n    `L${pt(mid, B[1] - s * r)}`,\n    `Q${pt(mid, B[1])} ${pt(mid + r, B[1])}`,\n    `L${pt(B[0], B[1])}`,\n  ].join(\" \");\n}\n\n/**\n * A view transform that fits the layout (or centres one node, for \"search glides the viewport\n * to the match\") inside a `width × height` viewport. Apply as `translate(x, y) scale(scale)`;\n * spring between two results to glide.\n */\nexport function fitView(\n  layout: Pick<FlowLayout, \"width\" | \"height\" | \"byId\">,\n  viewport: { width: number; height: number },\n  { focus, padding = 16, maxScale = 1, minScale = 0.25 }: { focus?: string; padding?: number; maxScale?: number; minScale?: number } = {},\n) {\n  const fit = Math.min((viewport.width - 2 * padding) / Math.max(1, layout.width), (viewport.height - 2 * padding) / Math.max(1, layout.height));\n  const scale = Math.max(minScale, Math.min(maxScale, fit));\n  const node = focus ? layout.byId.get(focus) : undefined;\n  const cx = node ? node.x + node.width / 2 : layout.width / 2;\n  const cy = node ? node.y + node.height / 2 : layout.height / 2;\n  return { scale, x: viewport.width / 2 - cx * scale, y: viewport.height / 2 - cy * scale };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/flow-core/layout.ts"
    },
    {
      "path": "src/registry/eased/_primitives/flow-core/status.ts",
      "content": "import { summarizeSteps, type StepSummary } from \"../thinking-core/steps\";\nimport { SETTLED_STEP, type StepStatus } from \"../thinking-core/types\";\nimport type { FlowEdge, FlowGraph, FlowNode } from \"./types\";\n\nconst statusOf = (n: FlowNode | undefined): StepStatus => n?.status ?? \"pending\";\n\n/**\n * An edge's status from its two nodes, so a renderer only has to set node statuses:\n *\n *   from complete, to active          → active    (the hand-off is happening: fill and flow)\n *   from complete, to skipped         → skipped\n *   from complete, anything else      → complete  (the dependency is met)\n *   from error or skipped             → skipped   (nothing will flow down it)\n *   from pending or active            → pending\n */\nexport function edgeStatus(from: StepStatus, to: StepStatus): StepStatus {\n  if (from === \"complete\") return to === \"active\" ? \"active\" : to === \"skipped\" ? \"skipped\" : \"complete\";\n  if (from === \"error\" || from === \"skipped\") return \"skipped\";\n  return \"pending\";\n}\n\n/** `edgeStatus` for an edge in a graph, honouring an explicit `edge.status`. */\nexport function resolveEdgeStatus(graph: FlowGraph, edge: FlowEdge, byId = nodeMap(graph)): StepStatus {\n  return edge.status ?? edgeStatus(statusOf(byId.get(edge.from)), statusOf(byId.get(edge.to)));\n}\n\n/** Where an edge's `pathLength` should be for a status: drawn for active and complete, empty otherwise. */\nexport function edgeProgressTarget(status: StepStatus): number {\n  return status === \"active\" || status === \"complete\" ? 1 : 0;\n}\n\nexport const nodeMap = <D>(graph: FlowGraph<D>) => new Map(graph.nodes.map((n) => [n.id, n]));\n\n/** Ids of the nodes upstream of `id`. */\nexport function predecessors(graph: FlowGraph, id: string): string[] {\n  return graph.edges.filter((e) => e.to === id).map((e) => e.from);\n}\n\n/** Every node reachable downstream of `id` (not including it). */\nexport function descendants(graph: FlowGraph, id: string): Set<string> {\n  const seen = new Set<string>();\n  const stack = [id];\n  while (stack.length) {\n    const v = stack.pop()!;\n    for (const e of graph.edges) {\n      if (e.from !== v || seen.has(e.to) || e.to === id) continue;\n      seen.add(e.to);\n      stack.push(e.to);\n    }\n  }\n  return seen;\n}\n\n/** Pending nodes whose predecessors have all completed (or were skipped): what can start next. */\nexport function readyNodes<D>(graph: FlowGraph<D>): FlowNode<D>[] {\n  const byId = nodeMap(graph);\n  return graph.nodes.filter(\n    (n) =>\n      statusOf(n) === \"pending\" &&\n      predecessors(graph, n.id).every((p) => {\n        const s = statusOf(byId.get(p));\n        return s === \"complete\" || s === \"skipped\";\n      }) &&\n      // A node downstream of a failure never becomes ready.\n      !predecessors(graph, n.id).some((p) => statusOf(byId.get(p)) === \"error\"),\n  );\n}\n\n/**\n * Immutable status change for one node, stamping `startedAt` / `endedAt` like thinking-core's\n * `updateStep`. Setting `error` marks everything downstream that hasn't settled as `skipped`\n * unless `skipDownstream` is false.\n */\nexport function setNodeStatus<D>(graph: FlowGraph<D>, id: string, status: StepStatus, { now = Date.now(), skipDownstream = true } = {}): FlowGraph<D> {\n  const downstream = status === \"error\" && skipDownstream ? descendants(graph, id) : new Set<string>();\n  return {\n    ...graph,\n    nodes: graph.nodes.map((n) => {\n      if (n.id === id) return stamp(n, status, now);\n      if (downstream.has(n.id) && !SETTLED_STEP.has(statusOf(n))) return stamp(n, \"skipped\", now);\n      return n;\n    }),\n  };\n}\n\nfunction stamp<D>(n: FlowNode<D>, status: StepStatus, now: number): FlowNode<D> {\n  const prev = statusOf(n);\n  if (prev === status) return n;\n  const next = { ...n, status };\n  if (status === \"active\" || status === \"pending\") {\n    next.startedAt = status === \"active\" ? now : undefined;\n    next.endedAt = undefined;\n  }\n  if (SETTLED_STEP.has(status) && !SETTLED_STEP.has(prev)) next.endedAt = now;\n  return next;\n}\n\n/**\n * One beat of a run: settles every active node (`outcome` decides each one's fate, default\n * complete) and starts every node that became ready. Parallel branches advance together, a\n * failure skips its downstream. Drive a scripted CI or agent-run demo with it on a clock.\n *\n *   setGraph((g) => stepFlow(g, { outcome: (n) => (n.id === \"e2e\" ? \"error\" : \"complete\") }));\n */\nexport function stepFlow<D>(\n  graph: FlowGraph<D>,\n  { outcome = () => \"complete\", now = Date.now() }: { outcome?: (node: FlowNode<D>) => \"complete\" | \"error\" | \"skipped\"; now?: number } = {},\n): FlowGraph<D> {\n  let g = graph;\n  const active = g.nodes.filter((n) => statusOf(n) === \"active\");\n  for (const n of active) g = setNodeStatus(g, n.id, outcome(n), { now });\n  // On the very first beat nothing is active yet: start the sources.\n  for (const n of readyNodes(g)) g = setNodeStatus(g, n.id, \"active\", { now });\n  return g;\n}\n\n/** Every node back to pending, times cleared: the \"retry\" rewind. */\nexport function resetFlow<D>(graph: FlowGraph<D>): FlowGraph<D> {\n  return { ...graph, nodes: graph.nodes.map((n) => ({ ...n, status: \"pending\", startedAt: undefined, endedAt: undefined })) };\n}\n\n/** thinking-core's `summarizeSteps` over the nodes: progress, current, status, durationMs. */\nexport function summarizeFlow(graph: FlowGraph): StepSummary {\n  return summarizeSteps(graph.nodes.map((n) => ({ id: n.id, label: n.label, status: statusOf(n), startedAt: n.startedAt, endedAt: n.endedAt })));\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/flow-core/status.ts"
    },
    {
      "path": "src/registry/eased/_primitives/flow-core/types.ts",
      "content": "import type { StepStatus } from \"../thinking-core/types\";\n\nexport type { StepStatus };\n\n/** One node of a flow: a CI job, an agent step, a payment hop, an approver. */\nexport type FlowNode<D = unknown> = {\n  /** Stable id: edges refer to it and renderers key by it. */\n  id: string;\n  label: string;\n  /** Where the node is. Shares thinking-core's frozen `StepStatus`. Default `pending`. */\n  status?: StepStatus;\n  /** A secondary line: \"2m 14s\", \"0.3% fee\", \"Partner\". */\n  detail?: string;\n  /** Epoch ms when it became active / settled (stamped by `setNodeStatus` and `stepFlow`). */\n  startedAt?: number;\n  endedAt?: number;\n  /** Size override for this node in the layout. */\n  width?: number;\n  height?: number;\n  /** Anything the renderer needs: an icon, an avatar, a fee. */\n  data?: D;\n};\n\n/** A directed edge `from → to`. */\nexport type FlowEdge = {\n  /** Defaults to `from->to`. */\n  id?: string;\n  from: string;\n  to: string;\n  label?: string;\n  /** Forces the edge's status; otherwise it is derived from its two nodes (`edgeStatus`). */\n  status?: StepStatus;\n};\n\nexport type FlowGraph<D = unknown> = { nodes: FlowNode<D>[]; edges: FlowEdge[] };\n\nexport const edgeId = (e: FlowEdge) => e.id ?? `${e.from}->${e.to}`;\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/flow-core/types.ts"
    }
  ],
  "type": "registry:lib"
}