{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flow",
  "type": "registry:component",
  "title": "Flow",
  "description": "A canvas of nodes joined by edges, that you pan, zoom and rearrange.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/flow/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/flow/index.tsx",
      "content": "'use client';\n\n/**\n * Flow — a canvas of nodes joined by edges, that you pan, zoom and rearrange.\n *\n * ```tsx\n * <Flow>\n *   <Flow.Background variant=\"dots\" />\n *   <Flow.Node id=\"source\" position={{ x: 24, y: 40 }}>\n *     <Flow.NodeContent>\n *       <Flow.NodeTitle>Source</Flow.NodeTitle>\n *       <Flow.NodeDescription>main</Flow.NodeDescription>\n *     </Flow.NodeContent>\n *   </Flow.Node>\n *   <Flow.Node id=\"build\" position={{ x: 24, y: 200 }}>…</Flow.Node>\n *   <Flow.Edge from=\"source\" to=\"build\" dashed animated arrow />\n *   <Flow.Controls />\n * </Flow>\n * ```\n *\n * ## The same parts as the app\n *\n * The parts and their props are the app library's Flow, so a graph written\n * for one reads the same in the other. What differs is the hand: here the\n * pane is dragged with a mouse, zoomed with a pinch or with Control and the\n * wheel, and every node can be reached and moved from the keyboard.\n *\n * ## Where the positions live\n *\n * A node's `position` is where it starts. From then on the canvas holds it:\n * a drag moves it there, and `onNodeDragEnd` says where it came to rest. Pass\n * a new `position` and the node goes to it, which is how nodes are driven\n * from outside.\n *\n * A node drawn inside a `Flow.Group` is positioned within it, and travels\n * with it. `confine` keeps its drag inside the group's box; `pinned` gives the\n * drag to the group instead, so the node moves only when the group does.\n *\n * Edges are drawn from the boxes of the nodes they join, measured off the\n * page. An edge therefore follows a node whatever is inside it, and redraws\n * as either end is dragged.\n *\n * ## Three gestures, one canvas\n *\n * The pane pans, a node drags, and a handle draws a new connection. Each is\n * taken by the innermost thing under the pointer, which is what a hand\n * expects.\n */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { tv } from 'tailwind-variants';\nimport {\n  anchorOf,\n  arrivalDirection,\n  arrowHeadPath,\n  autoSides,\n  bezierPath,\n  edgePath,\n  type FlowPoint,\n  type FlowRect,\n  type FlowSide,\n} from './flow-paths';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type * as ReactTypes from 'react';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type FlowEdgeVariant = 'bezier' | 'smoothstep' | 'step' | 'straight';\n\n/** Which of the theme's roles an edge is drawn in. */\nexport type FlowColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';\n\n/** Where the canvas is looking: how far it is panned, and how far zoomed. */\nexport interface FlowViewport {\n  x: number;\n  y: number;\n  zoom: number;\n}\n\nexport interface FlowNodePosition {\n  x: number;\n  y: number;\n}\n\n/** One end of an edge: a node, and optionally one of its handles by `id`. */\nexport interface FlowEndpoint {\n  node: string;\n  handle?: string;\n}\n\nexport type FlowEndpointReference = string | FlowEndpoint;\n\n/** What `onConnect` is handed when a connection lands. */\nexport interface FlowConnection {\n  /** Node the drag started from. */\n  source: string;\n  /** Handle it started from, when that handle has an `id`. */\n  sourceHandle?: string;\n  /** Node it was dropped on. */\n  target: string;\n  /** Handle it was dropped on, when it landed on one with an `id`. */\n  targetHandle?: string;\n}\n\nconst flowVariants = tv({\n  slots: {\n    root: 'relative h-96 w-full cursor-grab touch-none select-none overflow-hidden rounded-2xl border bg-background text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing',\n    underlay: 'pointer-events-none absolute inset-0 text-muted-foreground/35',\n    layer: 'absolute left-0 top-0 origin-top-left will-change-transform',\n    overlay: 'pointer-events-none absolute inset-0 *:pointer-events-auto',\n    node: 'absolute left-0 top-0 z-10 cursor-grab rounded-2xl border bg-card text-card-foreground shadow-sm outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing',\n    group:\n      'absolute left-0 top-0 cursor-grab rounded-3xl border border-dashed bg-muted/40 p-3 pt-8 outline-none focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing',\n    groupLabel: 'absolute start-4 top-2.5 text-xs font-medium uppercase tracking-wider text-muted-foreground',\n    // What goes inside a node. The padding is on the parts, so a header can\n    // draw its rule from edge to edge.\n    nodeHeader: 'flex items-center justify-between gap-3 border-b px-4 py-2.5',\n    // Inside a group item the item already has its padding.\n    nodeContent: 'px-4 py-3 in-data-[slot=flow-group-item]:p-0',\n    nodeTitle: 'text-sm font-medium',\n    nodeDescription: 'text-sm text-muted-foreground in-data-[slot=flow-group-item]:text-xs',\n    nodeBadge:\n      'rounded-full bg-[color-mix(in_oklab,var(--pu-color)_14%,transparent)] px-2 py-0.5 text-xs font-medium text-[var(--pu-color)]',\n    groupItem:\n      'flex items-center gap-3 rounded-xl border bg-card px-3 py-2.5 [&_svg]:size-4 [&_svg]:text-muted-foreground',\n    handle:\n      'absolute z-20 size-3 -translate-x-1/2 -translate-y-1/2 cursor-crosshair rounded-full border-2 border-background bg-muted-foreground outline-none transition-[scale,background-color] hover:scale-150 hover:bg-foreground focus-visible:ring-2 focus-visible:ring-ring',\n    edge: 'pointer-events-none absolute left-0 top-0 size-px overflow-visible text-[var(--pu-color)]',\n    controls:\n      'absolute flex flex-col divide-y divide-border overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm',\n    control:\n      'flex size-9 cursor-pointer items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground active:bg-muted [&_svg]:size-4',\n    minimap: 'absolute overflow-hidden rounded-xl border bg-card/90 shadow-sm backdrop-blur-sm',\n  },\n  variants: {\n    color: {\n      primary: { edge: '[--pu-color:var(--primary)]', nodeBadge: '[--pu-color:var(--primary)]' },\n      info: { edge: '[--pu-color:var(--info)]', nodeBadge: '[--pu-color:var(--info)]' },\n      success: { edge: '[--pu-color:var(--success)]', nodeBadge: '[--pu-color:var(--success)]' },\n      warning: { edge: '[--pu-color:var(--warning)]', nodeBadge: '[--pu-color:var(--warning)]' },\n      destructive: { edge: '[--pu-color:var(--destructive)]', nodeBadge: '[--pu-color:var(--destructive)]' },\n      // The colour of a hairline that is meant to be seen but not looked at.\n      muted: {\n        edge: '[--pu-color:var(--muted-foreground)]',\n        nodeBadge: 'bg-muted text-muted-foreground',\n      },\n    },\n    selected: { true: { node: 'z-20 ring-2 ring-ring' } },\n    position: {\n      'top-left': { controls: 'left-3 top-3', minimap: 'left-3 top-3' },\n      'top-right': { controls: 'right-3 top-3', minimap: 'right-3 top-3' },\n      'bottom-left': { controls: 'bottom-3 left-3', minimap: 'bottom-3 left-3' },\n      'bottom-right': { controls: 'bottom-3 right-3', minimap: 'bottom-3 right-3' },\n    },\n  },\n});\n\ninterface HandleEntry {\n  id?: string;\n  side: FlowSide;\n  offset: number;\n}\n\ninterface FlowContextValue {\n  viewport: FlowViewport;\n  /** Every box on the graph, in graph coordinates. What edges are drawn from. */\n  rects: Record<string, FlowRect>;\n  /** The same boxes as they were placed: a node in a group, relative to the group. */\n  local: Record<string, FlowRect>;\n  handles: Record<string, HandleEntry[]>;\n  locked: boolean;\n  size: { width: number; height: number };\n  underlay: HTMLElement | null;\n  overlay: HTMLElement | null;\n  place(id: string, position: FlowNodePosition, group?: string | null): void;\n  measure(id: string, width: number, height: number): void;\n  forget(id: string): void;\n  setHandle(node: string, key: string, entry: HandleEntry | null): void;\n  zoomBy(factor: number): void;\n  fit(): void;\n  setLocked(locked: boolean): void;\n  connectFrom(source: string, handle: HandleEntry, event: ReactPointerEvent): void;\n  onNodeDragEnd?: (id: string, position: FlowNodePosition) => void;\n}\n\nconst FlowContext = createContext<FlowContextValue | null>(null);\nconst NodeContext = createContext<string | null>(null);\n/** The group a node is drawn inside, if any. */\nconst GroupContext = createContext<string | null>(null);\n\nfunction useFlow(part: string): FlowContextValue {\n  const flow = useContext(FlowContext);\n  if (!flow) throw new Error(`${part} must be used inside a <Flow>.`);\n  return flow;\n}\n\nconst clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));\n\nconst OPPOSITE: Record<FlowSide, FlowSide> = {\n  top: 'bottom',\n  bottom: 'top',\n  left: 'right',\n  right: 'left',\n};\n\n/* -------------------------------------------------------------------------- */\n/* Root                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface FlowProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** Where the canvas looks to begin with. */\n  defaultViewport?: FlowViewport;\n  /** The furthest out it can be zoomed. */\n  minZoom?: number;\n  /** The furthest in it can be zoomed. */\n  maxZoom?: number;\n  /** Whether dragging the pane pans it. */\n  panOnDrag?: boolean;\n  /** Whether a pinch, or Control with the wheel, zooms it. */\n  zoomOnPinch?: boolean;\n  /** Fits every node into view once they have been measured. */\n  fitViewOnMount?: boolean;\n  /** The room left round the nodes when fitting them, in pixels. */\n  fitViewPadding?: number;\n  /** Called as the canvas is panned or zoomed. */\n  onViewportChange?: (viewport: FlowViewport) => void;\n  /** Called with a node's new position when a drag or a key press has moved it. */\n  onNodeDragEnd?: (id: string, position: FlowNodePosition) => void;\n  /** Called when a connection drawn from a handle lands on another node. */\n  onConnect?: (connection: FlowConnection) => void;\n  /** Decides whether a connection may land. Without it, any two different nodes may be joined. */\n  isValidConnection?: (connection: FlowConnection) => boolean;\n  /** What a screen reader calls the canvas. */\n  label?: string;\n  children?: ReactNode;\n}\n\nfunction FlowRoot({\n  defaultViewport = { x: 0, y: 0, zoom: 1 },\n  minZoom = 0.3,\n  maxZoom = 2.5,\n  panOnDrag = true,\n  zoomOnPinch = true,\n  fitViewOnMount = false,\n  fitViewPadding = 48,\n  onViewportChange,\n  onNodeDragEnd,\n  onConnect,\n  isValidConnection,\n  label = 'Diagram',\n  className,\n  children,\n  onKeyDown,\n  ...props\n}: FlowProps) {\n  const slots = flowVariants();\n  const root = useRef<HTMLDivElement>(null);\n  const [viewport, setViewportState] = useState(defaultViewport);\n  const [local, setRects] = useState<Record<string, FlowRect>>({});\n  const [parents, setParents] = useState<Record<string, string>>({});\n  // A node in a group is placed relative to it; everything that draws or\n  // fits reads the box where it actually is on the graph.\n  const rects = useMemo(() => {\n    const out: Record<string, FlowRect> = {};\n    for (const [id, box] of Object.entries(local)) {\n      const parent = parents[id] ? local[parents[id]] : undefined;\n      out[id] = parent ? { ...box, x: parent.x + box.x, y: parent.y + box.y } : box;\n    }\n    return out;\n  }, [local, parents]);\n  const [handles, setHandles] = useState<Record<string, HandleEntry[]>>({});\n  const [locked, setLocked] = useState(false);\n  const [size, setSize] = useState({ width: 0, height: 0 });\n  const [underlay, setUnderlay] = useState<HTMLElement | null>(null);\n  const [overlay, setOverlay] = useState<HTMLElement | null>(null);\n  /** A connection being drawn: where it leaves from and where the pointer is. */\n  const [wire, setWire] = useState<{ from: FlowPoint; side: FlowSide; to: FlowPoint } | null>(null);\n\n  const live = useRef({ viewport, rects, size, locked });\n  live.current = { viewport, rects, size, locked };\n  const callbacks = useRef({ onViewportChange, onConnect, isValidConnection });\n  callbacks.current = { onViewportChange, onConnect, isValidConnection };\n\n  const setViewport = useCallback((next: FlowViewport) => {\n    setViewportState(next);\n    callbacks.current.onViewportChange?.(next);\n  }, []);\n\n  /** A point on the page, as a point on the graph. */\n  const toGraph = useCallback((clientX: number, clientY: number): FlowPoint => {\n    const box = root.current?.getBoundingClientRect();\n    const { x, y, zoom } = live.current.viewport;\n    return { x: (clientX - (box?.left ?? 0) - x) / zoom, y: (clientY - (box?.top ?? 0) - y) / zoom };\n  }, []);\n\n  /** Zooms about a point on the canvas, so that point stays where it is. */\n  const zoomAbout = useCallback(\n    (factor: number, px: number, py: number) => {\n      const { x, y, zoom } = live.current.viewport;\n      const next = clamp(zoom * factor, minZoom, maxZoom);\n      const ratio = next / zoom;\n      setViewport({ x: px - (px - x) * ratio, y: py - (py - y) * ratio, zoom: next });\n    },\n    [minZoom, maxZoom, setViewport]\n  );\n\n  const zoomBy = useCallback(\n    (factor: number) => {\n      const { width, height } = live.current.size;\n      zoomAbout(factor, width / 2, height / 2);\n    },\n    [zoomAbout]\n  );\n\n  const fit = useCallback(() => {\n    const boxes = Object.values(live.current.rects).filter((box) => box.width > 0);\n    const { width, height } = live.current.size;\n    if (boxes.length === 0 || width === 0) return;\n    const left = Math.min(...boxes.map((box) => box.x));\n    const top = Math.min(...boxes.map((box) => box.y));\n    const right = Math.max(...boxes.map((box) => box.x + box.width));\n    const bottom = Math.max(...boxes.map((box) => box.y + box.height));\n    const zoom = clamp(\n      Math.min(\n        (width - fitViewPadding * 2) / Math.max(1, right - left),\n        (height - fitViewPadding * 2) / Math.max(1, bottom - top),\n        1\n      ),\n      minZoom,\n      maxZoom\n    );\n    setViewport({\n      x: (width - (right - left) * zoom) / 2 - left * zoom,\n      y: (height - (bottom - top) * zoom) / 2 - top * zoom,\n      zoom,\n    });\n  }, [fitViewPadding, minZoom, maxZoom, setViewport]);\n\n  // The canvas's own size, for fitting and for the minimap.\n  useLayoutEffect(() => {\n    const node = root.current;\n    if (!node) return;\n    const observer = new ResizeObserver(() => {\n      setSize({ width: node.clientWidth, height: node.clientHeight });\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  // Fitted once, when every node has been measured and the canvas has a size.\n  const fitted = useRef(false);\n  useEffect(() => {\n    if (!fitViewOnMount || fitted.current || size.width === 0) return;\n    const boxes = Object.values(rects);\n    if (boxes.length === 0 || boxes.some((box) => box.width === 0)) return;\n    fitted.current = true;\n    fit();\n  }, [fitViewOnMount, rects, size, fit]);\n\n  // Control with the wheel, which is also what a trackpad pinch sends. The\n  // plain wheel is left alone: a diagram in a page must not trap its scroll.\n  useEffect(() => {\n    const node = root.current;\n    if (!node || !zoomOnPinch) return;\n    const onWheel = (event: WheelEvent) => {\n      if (!event.ctrlKey && !event.metaKey) return;\n      event.preventDefault();\n      if (live.current.locked) return;\n      const box = node.getBoundingClientRect();\n      zoomAbout(Math.exp(-event.deltaY * 0.01), event.clientX - box.left, event.clientY - box.top);\n    };\n    node.addEventListener('wheel', onWheel, { passive: false });\n    return () => node.removeEventListener('wheel', onWheel);\n  }, [zoomOnPinch, zoomAbout]);\n\n  /* Panning, and pinching with two fingers. */\n  const pointers = useRef(new Map<number, { x: number; y: number }>());\n  const gesture = useRef<{ x: number; y: number; spread: number } | null>(null);\n\n  const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (event.target !== event.currentTarget && !(event.target as HTMLElement).hasAttribute('data-flow-pane')) return;\n    if (event.button !== 0 || locked) return;\n    event.currentTarget.setPointerCapture(event.pointerId);\n    pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });\n    gesture.current = null;\n  };\n\n  const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n    const held = pointers.current.get(event.pointerId);\n    if (!held) return;\n    const all = [...pointers.current.values()];\n    if (all.length === 1 && panOnDrag) {\n      const { x, y, zoom } = live.current.viewport;\n      setViewport({ x: x + event.clientX - held.x, y: y + event.clientY - held.y, zoom });\n    }\n    pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });\n    if (all.length === 2 && zoomOnPinch) {\n      const [a, b] = [...pointers.current.values()];\n      if (!a || !b) return;\n      const spread = Math.hypot(a.x - b.x, a.y - b.y);\n      const box = event.currentTarget.getBoundingClientRect();\n      const mid = { x: (a.x + b.x) / 2 - box.left, y: (a.y + b.y) / 2 - box.top };\n      if (gesture.current) zoomAbout(spread / gesture.current.spread, mid.x, mid.y);\n      gesture.current = { ...mid, spread };\n    }\n  };\n\n  const onPointerEnd = (event: ReactPointerEvent<HTMLDivElement>) => {\n    pointers.current.delete(event.pointerId);\n    gesture.current = null;\n  };\n\n  /* Drawing a connection from a handle. */\n  const connectFrom = useCallback(\n    (source: string, handle: HandleEntry, event: ReactPointerEvent) => {\n      const box = live.current.rects[source];\n      if (!box || live.current.locked) return;\n      const from = anchorOf(box, handle.side, handle.offset);\n      setWire({ from, side: handle.side, to: toGraph(event.clientX, event.clientY) });\n\n      const move = (next: PointerEvent) => {\n        setWire({ from, side: handle.side, to: toGraph(next.clientX, next.clientY) });\n      };\n      const up = (next: PointerEvent) => {\n        window.removeEventListener('pointermove', move);\n        window.removeEventListener('pointerup', up);\n        window.removeEventListener('pointercancel', up);\n        setWire(null);\n        if (next.type === 'pointercancel') return;\n        const under = document.elementFromPoint(next.clientX, next.clientY);\n        const onHandle = under?.closest<HTMLElement>('[data-flow-handle]');\n        const onNode = under?.closest<HTMLElement>('[data-flow-node]');\n        const target = onNode?.dataset.flowNode;\n        if (!target || target === source || !root.current?.contains(onNode ?? null)) return;\n        const connection: FlowConnection = {\n          source,\n          sourceHandle: handle.id,\n          target,\n          targetHandle: onHandle?.dataset.flowHandle || undefined,\n        };\n        if (callbacks.current.isValidConnection?.(connection) === false) return;\n        callbacks.current.onConnect?.(connection);\n      };\n      window.addEventListener('pointermove', move);\n      window.addEventListener('pointerup', up);\n      window.addEventListener('pointercancel', up);\n    },\n    [toGraph]\n  );\n\n  const place = useCallback((id: string, position: FlowNodePosition, group?: string | null) => {\n    if (group !== undefined) {\n      setParents((current) => {\n        if ((current[id] ?? null) === group) return current;\n        const { [id]: _was, ...rest } = current;\n        return group ? { ...rest, [id]: group } : rest;\n      });\n    }\n    setRects((current) => {\n      const before = current[id];\n      if (before && before.x === position.x && before.y === position.y) return current;\n      return {\n        ...current,\n        [id]: { x: position.x, y: position.y, width: before?.width ?? 0, height: before?.height ?? 0 },\n      };\n    });\n  }, []);\n\n  const measure = useCallback((id: string, width: number, height: number) => {\n    setRects((current) => {\n      const before = current[id];\n      if (!before || (before.width === width && before.height === height)) return current;\n      return { ...current, [id]: { ...before, width, height } };\n    });\n  }, []);\n\n  const forget = useCallback((id: string) => {\n    setParents((current) => {\n      if (!(id in current)) return current;\n      const { [id]: _gone, ...rest } = current;\n      return rest;\n    });\n    setRects((current) => {\n      if (!(id in current)) return current;\n      const { [id]: _gone, ...rest } = current;\n      return rest;\n    });\n  }, []);\n\n  const setHandle = useCallback((node: string, key: string, entry: HandleEntry | null) => {\n    setHandles((current) => {\n      const others = (current[node] ?? []).filter((handle) => (handle.id ?? handle.side) !== key);\n      return { ...current, [node]: entry ? [...others, entry] : others };\n    });\n  }, []);\n\n  const context = useMemo<FlowContextValue>(\n    () => ({\n      viewport,\n      rects,\n      local,\n      handles,\n      locked,\n      size,\n      underlay,\n      overlay,\n      place,\n      measure,\n      forget,\n      setHandle,\n      zoomBy,\n      fit,\n      setLocked,\n      connectFrom,\n      onNodeDragEnd,\n    }),\n    [\n      viewport,\n      rects,\n      local,\n      handles,\n      locked,\n      size,\n      underlay,\n      overlay,\n      place,\n      measure,\n      forget,\n      setHandle,\n      zoomBy,\n      fit,\n      connectFrom,\n      onNodeDragEnd,\n    ]\n  );\n\n  const keys = (event: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented || event.target !== event.currentTarget) return;\n    const pan: Record<string, [number, number]> = {\n      ArrowLeft: [40, 0],\n      ArrowRight: [-40, 0],\n      ArrowUp: [0, 40],\n      ArrowDown: [0, -40],\n    };\n    const by = pan[event.key];\n    if (by && !locked) {\n      event.preventDefault();\n      setViewport({ ...viewport, x: viewport.x + by[0], y: viewport.y + by[1] });\n    } else if (event.key === '+' || event.key === '=') zoomBy(1.3);\n    else if (event.key === '-') zoomBy(1 / 1.3);\n    else if (event.key === '0') fit();\n  };\n\n  return (\n    <FlowContext.Provider value={context}>\n      <div\n        ref={root}\n        role=\"application\"\n        aria-label={label}\n        aria-roledescription=\"Diagram\"\n        tabIndex={0}\n        data-locked={locked ? '' : undefined}\n        className={slots.root({ className })}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerEnd}\n        onPointerCancel={onPointerEnd}\n        onKeyDown={keys}\n        {...props}\n      >\n        <div ref={setUnderlay} data-flow-pane=\"\" className={slots.underlay()} />\n        <div\n          data-flow-layer=\"\"\n          className={slots.layer()}\n          style={{\n            transform: `translate(${viewport.x.toFixed(2)}px, ${viewport.y.toFixed(2)}px) scale(${viewport.zoom.toFixed(4)})`,\n          }}\n        >\n          {children}\n          {wire ? (\n            <svg aria-hidden=\"true\" className={flowVariants({ color: 'muted' }).edge()}>\n              <path\n                // It arrives facing back the way it left, which is the shape a\n                // wire has while it is still in the hand.\n                d={bezierPath(wire.from, wire.side, wire.to, OPPOSITE[wire.side], 0.25)}\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth={2}\n                strokeDasharray=\"6 6\"\n                strokeLinecap=\"round\"\n              />\n            </svg>\n          ) : null}\n        </div>\n        <div ref={setOverlay} className={slots.overlay()} />\n      </div>\n    </FlowContext.Provider>\n  );\n}\nFlowRoot.displayName = 'Flow';\n\n/* -------------------------------------------------------------------------- */\n/* Background                                                                 */\n/* -------------------------------------------------------------------------- */\n\nexport interface FlowBackgroundProps {\n  /** Dots, a grid of lines, small crosses, or nothing. */\n  variant?: 'dots' | 'lines' | 'cross' | 'none';\n  /** The distance from one mark to the next, in pixels at a zoom of 1. */\n  gap?: number;\n  /** How big a mark is: a dot's radius, a line's thickness, half a cross's arm. */\n  size?: number;\n  className?: string;\n}\n\n/** The pattern behind the graph, which moves and scales with it. */\nfunction FlowBackground({ variant = 'dots', gap = 24, size, className }: FlowBackgroundProps) {\n  const flow = useFlow('Flow.Background');\n  const id = useId();\n  if (!flow.underlay || variant === 'none') return null;\n  const { x, y, zoom } = flow.viewport;\n  const mark = size ?? (variant === 'dots' ? 1.2 : variant === 'lines' ? 1 : 3);\n\n  return createPortal(\n    <svg aria-hidden=\"true\" className={className ?? 'size-full'}>\n      <pattern\n        id={id}\n        width={gap}\n        height={gap}\n        patternUnits=\"userSpaceOnUse\"\n        patternTransform={`translate(${x.toFixed(2)} ${y.toFixed(2)}) scale(${zoom.toFixed(4)})`}\n      >\n        {variant === 'dots' ? <circle cx={gap / 2} cy={gap / 2} r={mark} fill=\"currentColor\" /> : null}\n        {variant === 'lines' ? (\n          <path d={`M${gap} 0H0V${gap}`} fill=\"none\" stroke=\"currentColor\" strokeWidth={mark} opacity=\"0.5\" />\n        ) : null}\n        {variant === 'cross' ? (\n          <path\n            d={`M${gap / 2 - mark} ${gap / 2}h${mark * 2}M${gap / 2} ${gap / 2 - mark}v${mark * 2}`}\n            stroke=\"currentColor\"\n            strokeWidth=\"1\"\n            strokeLinecap=\"round\"\n          />\n        ) : null}\n      </pattern>\n      <rect width=\"100%\" height=\"100%\" fill={`url(#${id})`} />\n    </svg>,\n    flow.underlay\n  );\n}\nFlowBackground.displayName = 'Flow.Background';\n\n/* -------------------------------------------------------------------------- */\n/* Node and Group                                                             */\n/* -------------------------------------------------------------------------- */\n\nexport interface FlowNodeProps extends Omit<ComponentProps<'div'>, 'id'> {\n  /** What edges and `onNodeDragEnd` call this node. */\n  id: string;\n  /** Where its top left corner is, on the graph. */\n  position: FlowNodePosition;\n  /** Whether it can be dragged and moved with the arrow keys. */\n  draggable?: boolean;\n  /**\n   * Inside a `Flow.Group`, stop a drag at the group's edge instead of letting\n   * the node leave it. Does nothing outside a group.\n   */\n  confine?: boolean;\n  /**\n   * Inside a `Flow.Group`, take no drag of its own: a drag that starts on it\n   * moves the group, and the node goes with it.\n   */\n  pinned?: boolean;\n  /** Draws it as the one that has been picked. */\n  selected?: boolean;\n  /** Called when it is pressed without being dragged, or with Enter. */\n  onPress?: () => void;\n  /** Called when Delete or Backspace is pressed on it. */\n  onDelete?: () => void;\n  /** How far one arrow key press moves it, in pixels. Shift moves four times as far. */\n  moveStep?: number;\n}\n\n/** What a node and a group share: a box on the graph that can be moved. */\nfunction useMovable({\n  id,\n  position,\n  draggable,\n  confine,\n  pinned,\n  group,\n  onPress,\n  onDelete,\n  moveStep,\n}: Pick<FlowNodeProps, 'id' | 'position' | 'draggable' | 'confine' | 'pinned' | 'onPress' | 'onDelete' | 'moveStep'> & {\n  group: string | null;\n}) {\n  const flow = useFlow('Flow.Node');\n  const element = useRef<HTMLDivElement>(null);\n  const { place, measure, forget } = flow;\n  const step = moveStep ?? 24;\n  const movable = draggable !== false && !pinned && !flow.locked;\n\n  useLayoutEffect(() => {\n    place(id, position, group);\n    // Only when the caller's position changes, not on every drawing.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [id, position.x, position.y, group, place]);\n\n  /** Where a move would put it, held inside its group when it is confined. */\n  const within = (next: FlowNodePosition): FlowNodePosition => {\n    const box = group && confine ? flow.local[group] : undefined;\n    if (!box || box.width === 0) return next;\n    const self = flow.local[id];\n    return {\n      x: clamp(next.x, 0, Math.max(0, box.width - (self?.width ?? 0))),\n      y: clamp(next.y, 0, Math.max(0, box.height - (self?.height ?? 0))),\n    };\n  };\n\n  useLayoutEffect(() => {\n    const node = element.current;\n    if (!node) return;\n    const read = () => measure(id, node.offsetWidth, node.offsetHeight);\n    read();\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [id, measure]);\n\n  useEffect(() => () => forget(id), [id, forget]);\n\n  const at = flow.local[id] ?? { ...position, width: 0, height: 0 };\n  const drag = useRef<{ x: number; y: number; from: FlowNodePosition; moved: boolean } | null>(null);\n  const latest = useRef(at);\n  latest.current = at;\n\n  const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (event.button !== 0) return;\n    // A handle starts a connection, and a control inside a node is pressed.\n    if (\n      (event.target as HTMLElement).closest('[data-flow-handle], button, a, input, textarea, select, [data-no-drag]')\n    ) {\n      return;\n    }\n    // A pinned node lets the press through to the group it is in, which\n    // takes the drag.\n    if (pinned && group) return;\n    // Kept from the pane, and from a group this node may be sitting in.\n    event.stopPropagation();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    drag.current = { x: event.clientX, y: event.clientY, from: { x: at.x, y: at.y }, moved: false };\n  };\n\n  const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n    const state = drag.current;\n    if (!state) return;\n    const dx = event.clientX - state.x;\n    const dy = event.clientY - state.y;\n    if (!state.moved && Math.hypot(dx, dy) < 3) return;\n    state.moved = true;\n    if (!movable) return;\n    place(id, within({ x: state.from.x + dx / flow.viewport.zoom, y: state.from.y + dy / flow.viewport.zoom }));\n  };\n\n  const onPointerUp = () => {\n    const state = drag.current;\n    drag.current = null;\n    if (!state) return;\n    if (!state.moved) onPress?.();\n    else if (movable) {\n      flow.onNodeDragEnd?.(id, { x: latest.current.x, y: latest.current.y });\n    }\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.target !== event.currentTarget) return;\n    const moves: Record<string, [number, number]> = {\n      ArrowLeft: [-1, 0],\n      ArrowRight: [1, 0],\n      ArrowUp: [0, -1],\n      ArrowDown: [0, 1],\n    };\n    const by = moves[event.key];\n    if (by) {\n      // Kept from the canvas, which would pan on the same key.\n      event.stopPropagation();\n      if (!movable) return;\n      event.preventDefault();\n      const far = step * (event.shiftKey ? 4 : 1);\n      const next = within({ x: at.x + by[0] * far, y: at.y + by[1] * far });\n      place(id, next);\n      flow.onNodeDragEnd?.(id, next);\n    } else if (event.key === 'Enter' || event.key === ' ') {\n      if (!onPress) return;\n      event.preventDefault();\n      onPress();\n    } else if ((event.key === 'Delete' || event.key === 'Backspace') && onDelete) {\n      event.preventDefault();\n      onDelete();\n    }\n  };\n\n  return {\n    element,\n    style: { translate: `${at.x.toFixed(2)}px ${at.y.toFixed(2)}px` },\n    handlers: {\n      onPointerDown,\n      onPointerMove,\n      onPointerUp,\n      onPointerCancel: () => {\n        drag.current = null;\n      },\n      onKeyDown,\n    },\n  };\n}\n\n/** A box on the graph. Fill it with the node parts below, or with anything of your own. */\nfunction FlowNode({\n  id,\n  position,\n  draggable = true,\n  confine = false,\n  pinned = false,\n  selected = false,\n  onPress,\n  onDelete,\n  moveStep,\n  className,\n  style,\n  children,\n  ...props\n}: FlowNodeProps) {\n  const group = useContext(GroupContext);\n  const movable = useMovable({ id, position, draggable, confine, pinned, group, onPress, onDelete, moveStep });\n  return (\n    <NodeContext.Provider value={id}>\n      <div\n        ref={movable.element}\n        data-flow-node={id}\n        data-pinned={pinned && group ? '' : undefined}\n        tabIndex={0}\n        aria-roledescription=\"Node\"\n        aria-selected={selected || undefined}\n        className={flowVariants({ selected }).node({ className })}\n        style={{ ...movable.style, ...style }}\n        {...movable.handlers}\n        {...props}\n      >\n        {children}\n      </div>\n    </NodeContext.Provider>\n  );\n}\nFlowNode.displayName = 'Flow.Node';\n\nexport interface FlowGroupProps extends Omit<FlowNodeProps, 'confine' | 'pinned'> {\n  /** Written in the corner of the group. */\n  label?: ReactNode;\n  /**\n   * A fixed size for the group. Give one when it holds `Flow.Node`s, which are\n   * placed inside it rather than laid out by it, so they do not size it.\n   */\n  size?: { width: number; height: number };\n}\n\n/**\n * A dashed box round a few things that belong together. It is a node like any\n * other: it is dragged as one, and an edge can be joined to it.\n */\nfunction FlowGroup({\n  id,\n  position,\n  draggable = true,\n  selected: _selected,\n  onPress,\n  onDelete,\n  moveStep,\n  label,\n  size,\n  className,\n  style,\n  children,\n  ...props\n}: FlowGroupProps) {\n  const movable = useMovable({ id, position, draggable, group: null, onPress, onDelete, moveStep });\n  return (\n    <GroupContext.Provider value={id}>\n      <NodeContext.Provider value={id}>\n        <div\n          ref={movable.element}\n          data-flow-node={id}\n          tabIndex={0}\n          aria-roledescription=\"Group\"\n          className={flowVariants().group({ className })}\n          style={{ ...movable.style, ...(size ? { width: size.width, height: size.height } : null), ...style }}\n          {...movable.handlers}\n          {...props}\n        >\n          {label !== undefined ? <span className={flowVariants().groupLabel()}>{label}</span> : null}\n          {children}\n        </div>\n      </NodeContext.Provider>\n    </GroupContext.Provider>\n  );\n}\nFlowGroup.displayName = 'Flow.Group';\n\n/* -------------------------------------------------------------------------- */\n/* What goes in a node                                                        */\n/* -------------------------------------------------------------------------- */\n\nexport interface FlowNodeHeaderProps extends ComponentProps<'div'> {}\n\n/** A strip across the top of a node, ruled off from the rest: a step number and a state. */\nfunction FlowNodeHeader({ className, render, ...props }: FlowNodeHeaderProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'flow-node-header', className: flowVariants().nodeHeader({ className }) }, props),\n  });\n}\nFlowNodeHeader.displayName = 'Flow.NodeHeader';\n\nexport interface FlowNodeContentProps extends ComponentProps<'div'> {}\n\n/** The body of a node, with the node's padding: a title over a description. */\nfunction FlowNodeContent({ className, render, ...props }: FlowNodeContentProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'flow-node-content', className: flowVariants().nodeContent({ className }) },\n      props\n    ),\n  });\n}\nFlowNodeContent.displayName = 'Flow.NodeContent';\n\nexport interface FlowNodeTitleProps extends ComponentProps<'p'> {}\n\n/** What the node is. */\nfunction FlowNodeTitle({ className, render, ...props }: FlowNodeTitleProps & RenderProp<'p'>) {\n  return useRender({\n    defaultTagName: 'p',\n    render,\n    props: mergeProps({ 'data-slot': 'flow-node-title', className: flowVariants().nodeTitle({ className }) }, props),\n  });\n}\nFlowNodeTitle.displayName = 'Flow.NodeTitle';\n\nexport interface FlowNodeDescriptionProps extends ComponentProps<'p'> {}\n\n/** A quieter line about the node. Smaller inside a `Flow.GroupItem`. */\nfunction FlowNodeDescription({ className, render, ...props }: FlowNodeDescriptionProps & RenderProp<'p'>) {\n  return useRender({\n    defaultTagName: 'p',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'flow-node-description', className: flowVariants().nodeDescription({ className }) },\n      props\n    ),\n  });\n}\nFlowNodeDescription.displayName = 'Flow.NodeDescription';\n\nexport interface FlowNodeBadgeProps extends Omit<ComponentProps<'span'>, 'color'> {\n  /** Which of the theme's roles it is tinted with. `muted` is grey. Default `muted`. */\n  color?: FlowColor | 'muted';\n}\n\n/** A word in a pill, for a node's state: done, now, waiting. */\nfunction FlowNodeBadge({ color = 'muted', className, render, ...props }: FlowNodeBadgeProps & RenderProp<'span'>) {\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'flow-node-badge', className: flowVariants({ color }).nodeBadge({ className }) },\n      props\n    ),\n  });\n}\nFlowNodeBadge.displayName = 'Flow.NodeBadge';\n\nexport interface FlowGroupItemProps extends ComponentProps<'div'> {}\n\n/**\n * One thing inside a `Flow.Group` that is not a node of its own: an icon\n * beside a `Flow.NodeContent`. It moves with the group and edges cannot join it.\n */\nfunction FlowGroupItem({ className, render, ...props }: FlowGroupItemProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'flow-group-item', className: flowVariants().groupItem({ className }) }, props),\n  });\n}\nFlowGroupItem.displayName = 'Flow.GroupItem';\n\n/* -------------------------------------------------------------------------- */\n/* Handle                                                                     */\n/* -------------------------------------------------------------------------- */\n\nexport interface FlowHandleProps extends Omit<ComponentProps<'span'>, 'id'> {\n  /** A name for this handle, so an edge can be joined to it and not only to the node. */\n  id?: string;\n  /** Which face of the node it sits on. */\n  position?: FlowSide;\n  /** How far along that face, from 0 to 1. */\n  offset?: number;\n}\n\nconst SIDE_STYLE: Record<FlowSide, (offset: number) => { left: string; top: string }> = {\n  top: (offset) => ({ left: `${offset * 100}%`, top: '0%' }),\n  bottom: (offset) => ({ left: `${offset * 100}%`, top: '100%' }),\n  left: (offset) => ({ left: '0%', top: `${offset * 100}%` }),\n  right: (offset) => ({ left: '100%', top: `${offset * 100}%` }),\n};\n\n/** A point on a node's edge that a connection can be drawn from, or dropped on. */\nfunction FlowHandle({\n  id,\n  position = 'right',\n  offset = 0.5,\n  className,\n  style,\n  render,\n  ...props\n}: FlowHandleProps & RenderProp<'span'>) {\n  const flow = useFlow('Flow.Handle');\n  const node = useContext(NodeContext);\n  if (node === null) throw new Error('Flow.Handle must be used inside a <Flow.Node>.');\n  const { setHandle, connectFrom } = flow;\n\n  useEffect(() => {\n    const key = id ?? position;\n    setHandle(node, key, { id, side: position, offset });\n    return () => setHandle(node, key, null);\n  }, [node, id, position, offset, setHandle]);\n\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        'data-flow-handle': id ?? '',\n        'aria-hidden': 'true',\n        className: flowVariants().handle({ className }),\n        style: { ...SIDE_STYLE[position](offset), ...style },\n        onPointerDown: (event: ReactTypes.PointerEvent<HTMLSpanElement>) => {\n          if (event.button !== 0) return;\n          event.stopPropagation();\n          event.preventDefault();\n          connectFrom(node, { id, side: position, offset }, event);\n        },\n      },\n      props\n    ),\n  });\n}\nFlowHandle.displayName = 'Flow.Handle';\n\n/* -------------------------------------------------------------------------- */\n/* Edge                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface FlowEdgeProps {\n  /** Where it leaves from: a node's `id`, `\"node.handle\"` for one of its handles, or `{ node, handle }`. */\n  from: FlowEndpointReference;\n  /** Where it arrives. */\n  to: FlowEndpointReference;\n  /** A curve, a rounded right-angled route, a square one, or a straight line. */\n  variant?: FlowEdgeVariant;\n  /** Sets the dashes marching towards the target. */\n  animated?: boolean;\n  /** Draws it dashed. */\n  dashed?: boolean;\n  /** One of the theme's roles. Without it the edge is a quiet grey. */\n  color?: FlowColor;\n  /** How thick the line is, in pixels at a zoom of 1. */\n  width?: number;\n  /** Draws an arrowhead where it arrives. */\n  arrow?: boolean;\n  /** The face it leaves from. Worked out from where the two nodes are when left out. */\n  fromSide?: FlowSide;\n  /** The face it arrives at. */\n  toSide?: FlowSide;\n  /** How round the corners of a `smoothstep` route are. */\n  radius?: number;\n  /** How far a routed edge runs straight out of a node before it turns. */\n  gap?: number;\n  /** How far a `bezier` bulges when its target is behind its source. */\n  curvature?: number;\n}\n\n/**\n * `\"node\"`, `\"node.handle\"` or `{ node, handle }`. A string that is a node's\n * whole id names that node, so an id with a dot in it still works.\n */\nconst endpoint = (reference: FlowEndpointReference, rects: Record<string, FlowRect>): FlowEndpoint => {\n  if (typeof reference !== 'string') return reference;\n  const dot = reference.lastIndexOf('.');\n  if (dot <= 0 || reference in rects) return { node: reference };\n  return { node: reference.slice(0, dot), handle: reference.slice(dot + 1) };\n};\n\n/** A line from one node to another, which follows them as they move. */\nfunction FlowEdge({\n  from,\n  to,\n  variant = 'bezier',\n  animated = false,\n  dashed = false,\n  color,\n  width = 2,\n  arrow = false,\n  fromSide,\n  toSide,\n  radius = 8,\n  gap = 20,\n  curvature = 0.25,\n}: FlowEdgeProps) {\n  const flow = useFlow('Flow.Edge');\n  // Whether motion is wanted is only known in the browser.\n  const [still, setStill] = useState(true);\n  useEffect(() => {\n    const query = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const read = () => setStill(query.matches);\n    read();\n    query.addEventListener('change', read);\n    return () => query.removeEventListener('change', read);\n  }, []);\n\n  const source = endpoint(from, flow.rects);\n  const target = endpoint(to, flow.rects);\n  const a = flow.rects[source.node];\n  const b = flow.rects[target.node];\n  // Nothing to draw until both ends have been measured.\n  if (!a || !b || a.width === 0 || b.width === 0) return null;\n\n  const named = (node: string, handle?: string) =>\n    handle === undefined ? undefined : flow.handles[node]?.find((entry) => entry.id === handle);\n  const out = named(source.node, source.handle);\n  const into = named(target.node, target.handle);\n  const auto = autoSides(a, b);\n  const sideA = fromSide ?? out?.side ?? auto.from;\n  const sideB = toSide ?? into?.side ?? auto.to;\n  const start = anchorOf(a, sideA, out?.offset ?? 0.5);\n  const tip = anchorOf(b, sideB, into?.offset ?? 0.5);\n  // The line stops short of the tip when there is an arrowhead to finish it.\n  const head = arrow ? 9 : 0;\n  const direction = arrivalDirection(variant, start, tip, sideB);\n  const length = Math.hypot(direction.x, direction.y) || 1;\n  const end = { x: tip.x - (direction.x / length) * head * 0.6, y: tip.y - (direction.y / length) * head * 0.6 };\n  const d = edgePath(variant, start, sideA, end, sideB, curvature, radius, gap);\n  const dash = dashed || animated ? '6 6' : undefined;\n\n  return (\n    <svg aria-hidden=\"true\" className={flowVariants({ color: color ?? 'muted' }).edge()}>\n      <path\n        d={d}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={width}\n        strokeDasharray={dash}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        opacity={color ? 1 : 0.7}\n      >\n        {animated && !still ? (\n          <animate attributeName=\"stroke-dashoffset\" from=\"12\" to=\"0\" dur=\"0.6s\" repeatCount=\"indefinite\" />\n        ) : null}\n      </path>\n      {arrow ? (\n        <path d={arrowHeadPath(tip, direction.x, direction.y, head)} fill=\"currentColor\" opacity={color ? 1 : 0.7} />\n      ) : null}\n    </svg>\n  );\n}\nFlowEdge.displayName = 'Flow.Edge';\n\n/* -------------------------------------------------------------------------- */\n/* Controls and MiniMap                                                       */\n/* -------------------------------------------------------------------------- */\n\ntype FlowCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\nexport interface FlowControlsProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** Shows the two zoom buttons. */\n  zoom?: boolean;\n  /** Shows the button that fits every node into view. */\n  fit?: boolean;\n  /** Shows the button that locks the canvas against panning, zooming and dragging. */\n  lock?: boolean;\n  /** How much one press of a zoom button zooms. */\n  step?: number;\n  /** Which corner of the canvas they sit in. */\n  position?: FlowCorner;\n}\n\nconst icon = (path: ReactNode) => (\n  <svg\n    aria-hidden=\"true\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    {path}\n  </svg>\n);\n\n/** Buttons for what a pinch and a drag do, for anyone without either. */\nfunction FlowControls({\n  zoom = true,\n  fit = true,\n  lock = true,\n  step = 1.3,\n  position = 'bottom-right',\n  className,\n  ...props\n}: FlowControlsProps) {\n  const flow = useFlow('Flow.Controls');\n  const slots = flowVariants({ position });\n  if (!flow.overlay) return null;\n\n  return createPortal(\n    <div role=\"group\" aria-label=\"Canvas controls\" className={slots.controls({ className })} {...props}>\n      {zoom ? (\n        <>\n          <button type=\"button\" aria-label=\"Zoom in\" className={slots.control()} onClick={() => flow.zoomBy(step)}>\n            {icon(<path d=\"M12 5v14M5 12h14\" />)}\n          </button>\n          <button type=\"button\" aria-label=\"Zoom out\" className={slots.control()} onClick={() => flow.zoomBy(1 / step)}>\n            {icon(<path d=\"M5 12h14\" />)}\n          </button>\n        </>\n      ) : null}\n      {fit ? (\n        <button type=\"button\" aria-label=\"Fit everything in view\" className={slots.control()} onClick={flow.fit}>\n          {icon(<path d=\"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5\" />)}\n        </button>\n      ) : null}\n      {lock ? (\n        <button\n          type=\"button\"\n          aria-label=\"Lock the canvas\"\n          aria-pressed={flow.locked}\n          className={slots.control()}\n          onClick={() => flow.setLocked(!flow.locked)}\n        >\n          {icon(\n            <>\n              <rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"2\" />\n              <path d={flow.locked ? 'M8 11V8a4 4 0 0 1 8 0v3' : 'M8 11V8a4 4 0 0 1 7.5-2'} />\n            </>\n          )}\n        </button>\n      ) : null}\n    </div>,\n    flow.overlay\n  );\n}\nFlowControls.displayName = 'Flow.Controls';\n\nexport interface FlowMiniMapProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** How wide the map is, in pixels. */\n  width?: number;\n  /** How tall the map is, in pixels. */\n  height?: number;\n  /** Which corner of the canvas it sits in. */\n  position?: FlowCorner;\n}\n\n/** The whole graph, small, with the part in view marked on it. */\nfunction FlowMiniMap({\n  width = 120,\n  height = 80,\n  position = 'bottom-left',\n  className,\n  style,\n  ...props\n}: FlowMiniMapProps) {\n  const flow = useFlow('Flow.MiniMap');\n  const slots = flowVariants({ position });\n  if (!flow.overlay) return null;\n\n  const boxes = Object.values(flow.rects).filter((box) => box.width > 0);\n  const { x, y, zoom } = flow.viewport;\n  // What the canvas is showing, in the graph's own coordinates.\n  const view = { x: -x / zoom, y: -y / zoom, width: flow.size.width / zoom, height: flow.size.height / zoom };\n  const all = [...boxes, view];\n  const left = Math.min(...all.map((box) => box.x));\n  const top = Math.min(...all.map((box) => box.y));\n  const right = Math.max(...all.map((box) => box.x + box.width));\n  const bottom = Math.max(...all.map((box) => box.y + box.height));\n  const pad = 20;\n\n  return createPortal(\n    <div aria-hidden=\"true\" className={slots.minimap({ className })} style={{ width, height, ...style }} {...props}>\n      <svg\n        viewBox={`${(left - pad).toFixed(1)} ${(top - pad).toFixed(1)} ${(right - left + pad * 2).toFixed(1)} ${(bottom - top + pad * 2).toFixed(1)}`}\n        className=\"size-full\"\n      >\n        {boxes.map((box, index) => (\n          <rect\n            key={index}\n            x={box.x}\n            y={box.y}\n            width={box.width}\n            height={box.height}\n            rx=\"10\"\n            className=\"fill-muted-foreground/40\"\n          />\n        ))}\n        <rect\n          x={view.x}\n          y={view.y}\n          width={view.width}\n          height={view.height}\n          rx=\"8\"\n          className=\"fill-foreground/5 stroke-foreground/50\"\n          strokeWidth=\"2\"\n          vectorEffect=\"non-scaling-stroke\"\n        />\n      </svg>\n    </div>,\n    flow.overlay\n  );\n}\nFlowMiniMap.displayName = 'Flow.MiniMap';\n\nexport const Flow = Object.assign(FlowRoot, {\n  Background: FlowBackground,\n  Node: FlowNode,\n  Handle: FlowHandle,\n  Edge: FlowEdge,\n  Group: FlowGroup,\n  GroupItem: FlowGroupItem,\n  NodeHeader: FlowNodeHeader,\n  NodeContent: FlowNodeContent,\n  NodeTitle: FlowNodeTitle,\n  NodeDescription: FlowNodeDescription,\n  NodeBadge: FlowNodeBadge,\n  Controls: FlowControls,\n  MiniMap: FlowMiniMap,\n});\n\n/* By name as well, for a Server Component, which cannot dot into a client\n   module. */\nexport {\n  FlowBackground,\n  FlowNode,\n  FlowHandle,\n  FlowEdge,\n  FlowGroup,\n  FlowGroupItem,\n  FlowNodeHeader,\n  FlowNodeContent,\n  FlowNodeTitle,\n  FlowNodeDescription,\n  FlowNodeBadge,\n  FlowControls,\n  FlowMiniMap,\n};\nexport type { FlowSide, FlowRect, FlowPoint };\n"
    },
    {
      "path": "components/panelui/flow/flow-paths.ts",
      "type": "registry:component",
      "target": "@components/panelui/flow/flow-paths.ts",
      "content": "/**\n * Edge geometry for Flow.\n *\n * Everything here is plain arithmetic on plain numbers: where on a node's face\n * an edge attaches, which faces two nodes should use when nobody has said, and\n * the path between two anchors in each of the four routings. It knows nothing\n * about React or the page, which is what lets it be tested by itself.\n *\n * Kept out of `index.tsx` so the component file stays about components. Named\n * `flow-paths` and not `paths`, because a copied component lands beside others\n * and `paths.ts` would be a land grab on a very common name.\n */\n\n/** Which face of a node an edge leaves from or arrives at. */\nexport type FlowSide = 'top' | 'right' | 'bottom' | 'left';\n\n/** A node's box in graph coordinates. */\nexport interface FlowRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport interface FlowPoint {\n  x: number;\n  y: number;\n}\n\n/** The outward unit normal of a face — which way an edge sets off. */\nexport function sideNormal(side: FlowSide): FlowPoint {\n  if (side === 'left') return { x: -1, y: 0 };\n  if (side === 'right') return { x: 1, y: 0 };\n  if (side === 'top') return { x: 0, y: -1 };\n  return { x: 0, y: 1 };\n}\n\n/**\n * Where on a face an edge attaches. `offset` runs 0–1 along the face, so two\n * handles on the same side can sit apart without either knowing about the other.\n */\nexport function anchorOf(rect: FlowRect, side: FlowSide, offset: number): FlowPoint {\n  if (side === 'top') {\n    return { x: rect.x + rect.width * offset, y: rect.y };\n  }\n  if (side === 'bottom') {\n    return { x: rect.x + rect.width * offset, y: rect.y + rect.height };\n  }\n  if (side === 'left') {\n    return { x: rect.x, y: rect.y + rect.height * offset };\n  }\n  return { x: rect.x + rect.width, y: rect.y + rect.height * offset };\n}\n\n/**\n * Pushes an anchor out along its own face's normal.\n *\n * For an end attached to something that draws its own border: landing exactly\n * on the border puts the line under the stroke, where it reads as stopping\n * short rather than as arriving.\n */\nexport function standOff(point: FlowPoint, side: FlowSide, by: number): FlowPoint {\n  if (by === 0) return point;\n  const normal = sideNormal(side);\n  return { x: point.x + normal.x * by, y: point.y + normal.y * by };\n}\n\n/**\n * The faces two nodes should use when nobody has said. Whichever axis they are\n * further apart on wins, so nodes side by side connect left-to-right and nodes\n * stacked connect top-to-bottom — and the edge changes its mind as they are\n * dragged past each other, which is what makes a hand-arranged graph stay\n * readable without anyone re-specifying anything.\n */\nexport function autoSides(from: FlowRect, to: FlowRect): { from: FlowSide; to: FlowSide } {\n  const dx = to.x + to.width / 2 - (from.x + from.width / 2);\n  const dy = to.y + to.height / 2 - (from.y + from.height / 2);\n\n  if (Math.abs(dx) > Math.abs(dy)) {\n    return dx >= 0 ? { from: 'right', to: 'left' } : { from: 'left', to: 'right' };\n  }\n  return dy >= 0 ? { from: 'bottom', to: 'top' } : { from: 'top', to: 'bottom' };\n}\n\n/** A line, for a graph where the routing is not the point. */\nexport function straightPath(from: FlowPoint, to: FlowPoint): string {\n  return `M${from.x},${from.y} L${to.x},${to.y}`;\n}\n\n/**\n * How far a bezier's control point sits from its anchor.\n *\n * With room between the two faces it is half the gap, which gives the familiar\n * S. When they overlap — the target is *behind* the source — the gap is\n * negative and half of it would fold the curve inside out, so the offset grows\n * with the square root of the overlap instead: enough to bulge the curve clear\n * of the node, never enough to run away as the overlap grows.\n */\nfunction controlOffset(distance: number, curvature: number): number {\n  if (distance >= 0) return 0.5 * distance;\n  return curvature * 25 * Math.sqrt(-distance);\n}\n\nfunction controlPoint(\n  point: FlowPoint,\n  side: FlowSide,\n  other: FlowPoint,\n  curvature: number\n): FlowPoint {\n  if (side === 'left') {\n    return { x: point.x - controlOffset(point.x - other.x, curvature), y: point.y };\n  }\n  if (side === 'right') {\n    return { x: point.x + controlOffset(other.x - point.x, curvature), y: point.y };\n  }\n  if (side === 'top') {\n    return { x: point.x, y: point.y - controlOffset(point.y - other.y, curvature) };\n  }\n  return { x: point.x, y: point.y + controlOffset(other.y - point.y, curvature) };\n}\n\n/** A cubic curve leaving and arriving perpendicular to each face. */\nexport function bezierPath(\n  from: FlowPoint,\n  fromSide: FlowSide,\n  to: FlowPoint,\n  toSide: FlowSide,\n  curvature: number\n): string {\n  const c1 = controlPoint(from, fromSide, to, curvature);\n  const c2 = controlPoint(to, toSide, from, curvature);\n  return `M${from.x},${from.y} C${c1.x},${c1.y} ${c2.x},${c2.y} ${to.x},${to.y}`;\n}\n\nfunction distanceBetween(a: FlowPoint, b: FlowPoint): number {\n  const dx = b.x - a.x;\n  const dy = b.y - a.y;\n  return Math.sqrt(dx * dx + dy * dy);\n}\n\n/**\n * One corner of a stepped path, rounded.\n *\n * The radius is capped at half of the shorter of the two segments meeting here,\n * so a corner between two short runs tucks in rather than overshooting into the\n * segment beyond it and drawing a path that doubles back.\n */\nfunction bend(a: FlowPoint, b: FlowPoint, c: FlowPoint, radius: number): string {\n  const size = Math.min(distanceBetween(a, b) / 2, distanceBetween(b, c) / 2, radius);\n\n  // Three collinear points are not a corner.\n  if ((a.x === b.x && b.x === c.x) || (a.y === b.y && b.y === c.y)) {\n    return `L${b.x},${b.y}`;\n  }\n\n  if (a.y === b.y) {\n    const xDir = a.x < c.x ? -1 : 1;\n    const yDir = a.y < c.y ? 1 : -1;\n    return `L${b.x + size * xDir},${b.y}Q${b.x},${b.y} ${b.x},${b.y + size * yDir}`;\n  }\n\n  const xDir = a.x < c.x ? 1 : -1;\n  const yDir = a.y < c.y ? -1 : 1;\n  return `L${b.x},${b.y + size * yDir}Q${b.x},${b.y} ${b.x + size * xDir},${b.y}`;\n}\n\n/**\n * An orthogonal route with rounded corners — the one that reads as wiring.\n *\n * Both ends first step `gap` points straight out along their own face, so the\n * edge leaves the node square-on instead of grazing its corner. From there the\n * two are joined by a single dog-leg, split across the axis they are further\n * apart on.\n *\n * `radius` of 0 gives hard corners, which is the stepped variant.\n */\nexport function smoothStepPath(\n  from: FlowPoint,\n  fromSide: FlowSide,\n  to: FlowPoint,\n  toSide: FlowSide,\n  radius: number,\n  gap: number\n): string {\n  const fromNormal = sideNormal(fromSide);\n  const toNormal = sideNormal(toSide);\n\n  let fromGap = {\n    x: from.x + fromNormal.x * gap,\n    y: from.y + fromNormal.y * gap,\n  };\n  let toGap = { x: to.x + toNormal.x * gap, y: to.y + toNormal.y * gap };\n\n  // The axis the two gap points are further apart on is the one the dog-leg\n  // travels along.\n  const horizontal = Math.abs(toGap.x - fromGap.x) > Math.abs(toGap.y - fromGap.y);\n  const facing =\n    (fromNormal.x !== 0 && toNormal.x !== 0 && fromNormal.x !== toNormal.x) ||\n    (fromNormal.y !== 0 && toNormal.y !== 0 && fromNormal.y !== toNormal.y);\n\n  let middle: FlowPoint[];\n\n  if (facing) {\n    // Faces pointing at each other: meet in the middle of the run between them.\n    const midX = (fromGap.x + toGap.x) / 2;\n    const midY = (fromGap.y + toGap.y) / 2;\n    middle =\n      fromNormal.x !== 0\n        ? [\n            { x: midX, y: fromGap.y },\n            { x: midX, y: toGap.y },\n          ]\n        : [\n            { x: fromGap.x, y: midY },\n            { x: toGap.x, y: midY },\n          ];\n  } else {\n    // Faces pointing the same way, or at right angles: one corner is enough.\n    // Which of the two possible corners depends on the travelling axis.\n    middle = horizontal ? [{ x: toGap.x, y: fromGap.y }] : [{ x: fromGap.x, y: toGap.y }];\n\n    // Two handles on the same face, closer together than the gap: the gap\n    // points sit between the anchors and the route folds back over itself.\n    // Push them out far enough to clear.\n    if (fromSide === toSide) {\n      const axis = fromNormal.x !== 0 ? 'x' : 'y';\n      const apart = Math.abs(from[axis] - to[axis]);\n      if (apart <= gap) {\n        const push = Math.min(gap - 1, gap - apart);\n        if (axis === 'x') {\n          fromGap = { x: fromGap.x + fromNormal.x * push, y: fromGap.y };\n          toGap = { x: toGap.x + toNormal.x * push, y: toGap.y };\n          middle = [{ x: Math.max(fromGap.x, toGap.x), y: fromGap.y }];\n        } else {\n          fromGap = { x: fromGap.x, y: fromGap.y + fromNormal.y * push };\n          toGap = { x: toGap.x, y: toGap.y + toNormal.y * push };\n          middle = [{ x: fromGap.x, y: Math.max(fromGap.y, toGap.y) }];\n        }\n      }\n    }\n  }\n\n  const points: FlowPoint[] = [from, fromGap, ...middle, toGap, to];\n\n  // Drop points that repeat the one before them — a duplicate reads as a\n  // zero-length segment and makes the bend either side of it collapse.\n  const route: FlowPoint[] = [];\n  for (let i = 0; i < points.length; i += 1) {\n    const point = points[i]!;\n    const previous = route[route.length - 1];\n    if (previous && previous.x === point.x && previous.y === point.y) continue;\n    route.push(point);\n  }\n\n  if (route.length < 2) return `M${from.x},${from.y}`;\n\n  let d = `M${route[0]!.x},${route[0]!.y}`;\n  for (let i = 1; i < route.length - 1; i += 1) {\n    d +=\n      radius > 0\n        ? bend(route[i - 1]!, route[i]!, route[i + 1]!, radius)\n        : `L${route[i]!.x},${route[i]!.y}`;\n  }\n  const last = route[route.length - 1]!;\n  d += `L${last.x},${last.y}`;\n  return d;\n}\n\n/**\n * The `d` string for an edge between two anchors, in whichever shape was asked\n * for. One entry point, so an edge's path is a single call.\n */\nexport function edgePath(\n  variant: 'bezier' | 'smoothstep' | 'step' | 'straight',\n  from: FlowPoint,\n  fromSide: FlowSide,\n  to: FlowPoint,\n  toSide: FlowSide,\n  curvature: number,\n  radius: number,\n  gap: number\n): string {\n  if (variant === 'straight') return straightPath(from, to);\n  if (variant === 'bezier') return bezierPath(from, fromSide, to, toSide, curvature);\n  return smoothStepPath(from, fromSide, to, toSide, variant === 'step' ? 0 : radius, gap);\n}\n\n/** Length of an edge's bounding diagonal — enough to size a dash cycle by. */\nexport function edgeSpan(from: FlowPoint, to: FlowPoint): number {\n  return distanceBetween(from, to);\n}\n\n/**\n * The arrowhead at an edge's target, as its own closed path.\n *\n * SVG has `marker-end` for exactly this, and it is the obvious way to do it.\n * It is not used, because a path carrying a marker reference stops picking up\n * new geometry on re-render — the arrow-bearing edges freeze where they were\n * first drawn while arrow-less ones follow their nodes. Drawing the triangle\n * as an ordinary path costs nine numbers and behaves.\n *\n * `dirX`/`dirY` is the unit direction of travel *into* the target.\n */\nexport function arrowHeadPath(\n  tip: FlowPoint,\n  dirX: number,\n  dirY: number,\n  size: number\n): string {\n  const length = Math.sqrt(dirX * dirX + dirY * dirY) || 1;\n  const ux = dirX / length;\n  const uy = dirY / length;\n  // The perpendicular, for the two back corners.\n  const px = -uy;\n  const py = ux;\n\n  const baseX = tip.x - ux * size;\n  const baseY = tip.y - uy * size;\n  const half = size * 0.45;\n\n  return (\n    `M${tip.x},${tip.y} ` +\n    `L${baseX + px * half},${baseY + py * half} ` +\n    `L${baseX - px * half},${baseY - py * half} Z`\n  );\n}\n\n/**\n * Which way an edge is travelling as it arrives. Every routing but `straight`\n * comes in perpendicular to the target's face, so the face decides it; a\n * straight line comes in along itself.\n */\nexport function arrivalDirection(\n  variant: 'bezier' | 'smoothstep' | 'step' | 'straight',\n  from: FlowPoint,\n  to: FlowPoint,\n  toSide: FlowSide\n): { x: number; y: number } {\n  if (variant === 'straight') {\n    return { x: to.x - from.x, y: to.y - from.y };\n  }\n  const normal = sideNormal(toSide);\n  // The face points outward; the edge arrives against it.\n  return { x: -normal.x, y: -normal.y };\n}\n"
    }
  ]
}
