import {
  cn
} from "./chunk-ZAUUGK2Y.js";

// src/workflow/utils/paths.ts
function buildEdgePath(routing, start, startSide, end, endSide, options = {}) {
  switch (routing) {
    case "step":
      return stepPath(start, startSide, end, endSide);
    case "smoothstep":
      return smoothStepPath(start, startSide, end, endSide, options.borderRadius ?? 8);
    case "straight":
      return straightPath(start, end);
    default:
      return bezierPath(start, startSide, end, endSide, options.curvature ?? 0.25);
  }
}
function bezierPath(start, startSide, end, endSide, curvature = 0.25) {
  const sourceOffset = calcControlOffset(start, startSide, end, curvature);
  const targetOffset = calcControlOffset(end, endSide, start, curvature);
  const c1 = offsetAlongSide(start, startSide, sourceOffset);
  const c2 = offsetAlongSide(end, endSide, targetOffset);
  const d = `M ${start.x},${start.y} C ${c1.x},${c1.y} ${c2.x},${c2.y} ${end.x},${end.y}`;
  const midX = 0.125 * start.x + 0.375 * c1.x + 0.375 * c2.x + 0.125 * end.x;
  const midY = 0.125 * start.y + 0.375 * c1.y + 0.375 * c2.y + 0.125 * end.y;
  const tx = 3 * ((1 - 0.5) ** 2 * (c1.x - start.x) + 2 * (1 - 0.5) * 0.5 * (c2.x - c1.x) + 0.5 ** 2 * (end.x - c2.x));
  const ty = 3 * ((1 - 0.5) ** 2 * (c1.y - start.y) + 2 * (1 - 0.5) * 0.5 * (c2.y - c1.y) + 0.5 ** 2 * (end.y - c2.y));
  const midAngle = Math.atan2(ty, tx);
  return { d, midX, midY, midAngle };
}
function axisDistance(from, side, to) {
  switch (side) {
    case "top":
      return from.y - to.y;
    case "bottom":
      return to.y - from.y;
    case "left":
      return from.x - to.x;
    case "right":
      return to.x - from.x;
  }
}
function calcControlOffset(from, side, to, curvature) {
  const distance2 = axisDistance(from, side, to);
  if (distance2 >= 0) {
    return distance2 * 0.5 * (1 + curvature);
  }
  return curvature * 25 * Math.sqrt(-distance2);
}
function offsetAlongSide(p, side, magnitude) {
  switch (side) {
    case "top":
      return { x: p.x, y: p.y - magnitude };
    case "bottom":
      return { x: p.x, y: p.y + magnitude };
    case "left":
      return { x: p.x - magnitude, y: p.y };
    case "right":
      return { x: p.x + magnitude, y: p.y };
  }
}
function stepPath(start, startSide, end, endSide) {
  const corners = computeStepCorners(start, startSide, end, endSide);
  const pts = [start, ...corners, end];
  const d = pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x},${p.y}`).join(" ");
  const { midX, midY, midAngle } = midpointOnPolyline(pts);
  return { d, midX, midY, midAngle };
}
function smoothStepPath(start, startSide, end, endSide, borderRadius = 8) {
  const corners = computeStepCorners(start, startSide, end, endSide);
  const pts = [start, ...corners, end];
  if (corners.length === 0) {
    return straightPath(start, end);
  }
  const segments = [`M ${pts[0].x},${pts[0].y}`];
  for (let i = 1; i < pts.length - 1; i++) {
    const prev = pts[i - 1];
    const here = pts[i];
    const next = pts[i + 1];
    const r = Math.min(borderRadius, distance(prev, here) / 2, distance(here, next) / 2);
    const enter = pointTowards(here, prev, r);
    const exit = pointTowards(here, next, r);
    segments.push(`L ${enter.x},${enter.y}`);
    segments.push(`Q ${here.x},${here.y} ${exit.x},${exit.y}`);
  }
  const last = pts[pts.length - 1];
  segments.push(`L ${last.x},${last.y}`);
  const d = segments.join(" ");
  const { midX, midY, midAngle } = midpointOnPolyline(pts);
  return { d, midX, midY, midAngle };
}
function straightPath(start, end) {
  const d = `M ${start.x},${start.y} L ${end.x},${end.y}`;
  return {
    d,
    midX: (start.x + end.x) / 2,
    midY: (start.y + end.y) / 2,
    midAngle: Math.atan2(end.y - start.y, end.x - start.x)
  };
}
function computeStepCorners(start, startSide, end, endSide) {
  const sourceVertical = startSide === "top" || startSide === "bottom";
  const targetVertical = endSide === "top" || endSide === "bottom";
  if (sourceVertical && targetVertical) {
    const midY = (start.y + end.y) / 2;
    return [
      { x: start.x, y: midY },
      { x: end.x, y: midY }
    ];
  }
  if (!sourceVertical && !targetVertical) {
    const midX = (start.x + end.x) / 2;
    return [
      { x: midX, y: start.y },
      { x: midX, y: end.y }
    ];
  }
  if (sourceVertical) {
    return [{ x: end.x, y: start.y }];
  }
  return [{ x: start.x, y: end.y }];
}
function midpointOnPolyline(pts) {
  if (pts.length === 2) {
    return {
      midX: (pts[0].x + pts[1].x) / 2,
      midY: (pts[0].y + pts[1].y) / 2,
      midAngle: Math.atan2(pts[1].y - pts[0].y, pts[1].x - pts[0].x)
    };
  }
  let totalLen = 0;
  const lens = [];
  for (let i = 0; i < pts.length - 1; i++) {
    const l = distance(pts[i], pts[i + 1]);
    lens.push(l);
    totalLen += l;
  }
  const half = totalLen / 2;
  let acc = 0;
  for (let i = 0; i < lens.length; i++) {
    if (acc + lens[i] >= half) {
      const seg = lens[i];
      const remaining = half - acc;
      const t = seg === 0 ? 0 : remaining / seg;
      const a = pts[i];
      const b = pts[i + 1];
      return {
        midX: a.x + (b.x - a.x) * t,
        midY: a.y + (b.y - a.y) * t,
        midAngle: Math.atan2(b.y - a.y, b.x - a.x)
      };
    }
    acc += lens[i];
  }
  return { midX: pts[0].x, midY: pts[0].y, midAngle: 0 };
}
function distance(a, b) {
  return Math.hypot(b.x - a.x, b.y - a.y);
}
function pointTowards(from, to, dist) {
  const d = distance(from, to);
  if (d === 0) return from;
  const t = dist / d;
  return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
}

// src/workflow/hooks/useFlow.ts
import { createContext, useContext } from "react";
var FlowInstanceContext = createContext(null);
function useFlow() {
  const instance = useContext(FlowInstanceContext);
  if (!instance) {
    throw new Error("[@octaviaflow/core/workflow] useFlow() must be called inside <FlowCanvas>.");
  }
  return instance;
}

// src/workflow/store/changes.ts
function applyNodeChanges(nodes, changes) {
  if (changes.length === 0) return nodes;
  const byId = /* @__PURE__ */ new Map();
  const adds = [];
  const removes = /* @__PURE__ */ new Set();
  let touched = false;
  for (const change2 of changes) {
    switch (change2.type) {
      case "add":
        adds.push(change2.item);
        touched = true;
        break;
      case "remove":
        removes.add(change2.id);
        touched = true;
        break;
      default: {
        const id = change2.id;
        const arr = byId.get(id);
        if (arr) arr.push(change2);
        else byId.set(id, [change2]);
        touched = true;
        break;
      }
    }
  }
  if (!touched) return nodes;
  const next = [];
  for (const node of nodes) {
    if (removes.has(node.id)) continue;
    const patches = byId.get(node.id);
    if (!patches) {
      next.push(node);
      continue;
    }
    let n = node;
    for (const patch of patches) {
      switch (patch.type) {
        case "position":
          n = {
            ...n,
            position: patch.position,
            dragging: patch.dragging
          };
          break;
        case "dimensions":
          n = {
            ...n,
            width: patch.dimensions.width,
            height: patch.dimensions.height
          };
          break;
        case "select":
          if (n.selected === patch.selected) break;
          n = { ...n, selected: patch.selected };
          break;
        case "replace":
          n = patch.item;
          break;
      }
    }
    next.push(n);
  }
  for (const add of adds) next.push(add);
  return next;
}
function applyEdgeChanges(edges, changes) {
  if (changes.length === 0) return edges;
  const byId = /* @__PURE__ */ new Map();
  const adds = [];
  const removes = /* @__PURE__ */ new Set();
  let touched = false;
  for (const change2 of changes) {
    switch (change2.type) {
      case "add":
        adds.push(change2.item);
        touched = true;
        break;
      case "remove":
        removes.add(change2.id);
        touched = true;
        break;
      default: {
        const arr = byId.get(change2.id);
        if (arr) arr.push(change2);
        else byId.set(change2.id, [change2]);
        touched = true;
        break;
      }
    }
  }
  if (!touched) return edges;
  const next = [];
  for (const edge of edges) {
    if (removes.has(edge.id)) continue;
    const patches = byId.get(edge.id);
    if (!patches) {
      next.push(edge);
      continue;
    }
    let e = edge;
    for (const patch of patches) {
      switch (patch.type) {
        case "select":
          if (e.selected === patch.selected) break;
          e = { ...e, selected: patch.selected };
          break;
        case "replace":
          e = patch.item;
          break;
      }
    }
    next.push(e);
  }
  for (const add of adds) next.push(add);
  return next;
}
var change = {
  node: {
    add(item) {
      return { type: "add", item };
    },
    remove(id) {
      return { type: "remove", id };
    },
    position(id, position, dragging) {
      return { type: "position", id, position, dragging };
    },
    dimensions(id, dimensions) {
      return { type: "dimensions", id, dimensions };
    },
    select(id, selected) {
      return { type: "select", id, selected };
    },
    replace(id, item) {
      return { type: "replace", id, item };
    }
  },
  edge: {
    add(item) {
      return { type: "add", item };
    },
    remove(id) {
      return { type: "remove", id };
    },
    select(id, selected) {
      return { type: "select", id, selected };
    },
    replace(id, item) {
      return { type: "replace", id, item };
    }
  }
};

// src/workflow/utils/geometry.ts
var DEFAULT_NODE_WIDTH = 368;
var DEFAULT_NODE_HEIGHT = 96;
var COLLAPSED_GROUP_HEIGHT = 36;
var COLLAPSED_FOREACH_HEIGHT = 40;
function effectiveHeight(node) {
  const data = node.data;
  if (data?.collapsed) {
    if (node.type === "group") return COLLAPSED_GROUP_HEIGHT;
    if (node.type === "forEach") return COLLAPSED_FOREACH_HEIGHT;
  }
  return node.height ?? DEFAULT_NODE_HEIGHT;
}
function handleCentre(node, side, index, total) {
  const w = node.width ?? DEFAULT_NODE_WIDTH;
  const h = effectiveHeight(node);
  const x0 = node.position.x;
  const y0 = node.position.y;
  const denom = total + 1;
  const ratio = (index + 1) / denom;
  switch (side) {
    case "top":
      return { x: x0 + w * ratio, y: y0 };
    case "bottom":
      return { x: x0 + w * ratio, y: y0 + h };
    case "left":
      return { x: x0, y: y0 + h * ratio };
    case "right":
      return { x: x0 + w, y: y0 + h * ratio };
  }
}
function bezierPath2(start, startSide, end, endSide) {
  const distance2 = Math.hypot(end.x - start.x, end.y - start.y);
  const magnitude = Math.max(40, Math.min(160, distance2 * 0.45));
  const c1 = offsetBySide(start, startSide, magnitude);
  const c2 = offsetBySide(end, endSide, magnitude);
  const d = `M ${start.x},${start.y} C ${c1.x},${c1.y} ${c2.x},${c2.y} ${end.x},${end.y}`;
  const midX = 0.125 * start.x + 0.375 * c1.x + 0.375 * c2.x + 0.125 * end.x;
  const midY = 0.125 * start.y + 0.375 * c1.y + 0.375 * c2.y + 0.125 * end.y;
  return { d, midX, midY };
}
function offsetBySide(p, side, m) {
  switch (side) {
    case "top":
      return { x: p.x, y: p.y - m };
    case "bottom":
      return { x: p.x, y: p.y + m };
    case "left":
      return { x: p.x - m, y: p.y };
    case "right":
      return { x: p.x + m, y: p.y };
  }
}
function screenToFlow(p, vp) {
  return {
    x: (p.x - vp.x) / vp.zoom,
    y: (p.y - vp.y) / vp.zoom
  };
}
function flowToScreen(p, vp) {
  return {
    x: p.x * vp.zoom + vp.x,
    y: p.y * vp.zoom + vp.y
  };
}

// src/workflow/utils/parenting.ts
function descendantsOf(nodeId, nodes) {
  const childrenByParent = /* @__PURE__ */ new Map();
  for (const n of nodes) {
    if (!n.parentId) continue;
    const arr = childrenByParent.get(n.parentId);
    if (arr) arr.push(n);
    else childrenByParent.set(n.parentId, [n]);
  }
  const out = [];
  const stack = [nodeId];
  const visited = /* @__PURE__ */ new Set([nodeId]);
  while (stack.length) {
    const cur = stack.pop();
    if (cur === void 0) break;
    const list = childrenByParent.get(cur);
    if (!list) continue;
    for (const c of list) {
      if (visited.has(c.id)) continue;
      visited.add(c.id);
      out.push(c);
      stack.push(c.id);
    }
  }
  return out;
}
function findAncestor(node, nodes, predicate) {
  let cursor = node;
  const seen = /* @__PURE__ */ new Set();
  while (cursor?.parentId) {
    if (seen.has(cursor.parentId)) return void 0;
    seen.add(cursor.parentId);
    const parent = nodes.find((n) => n.id === cursor.parentId);
    if (!parent) return void 0;
    if (predicate(parent)) return parent;
    cursor = parent;
  }
  return void 0;
}
function clampToParentExtent(node, proposed, nodes) {
  if (node.extent !== "parent" || !node.parentId) return proposed;
  const parent = nodes.find((n) => n.id === node.parentId);
  if (!parent) return proposed;
  const pw = parent.width ?? DEFAULT_NODE_WIDTH;
  const ph = effectiveHeight(parent);
  const w = node.width ?? DEFAULT_NODE_WIDTH;
  const h = effectiveHeight(node);
  const minX = parent.position.x;
  const minY = parent.position.y;
  const maxX = parent.position.x + pw - w;
  const maxY = parent.position.y + ph - h;
  return {
    x: Math.max(minX, Math.min(maxX, proposed.x)),
    y: Math.max(minY, Math.min(maxY, proposed.y))
  };
}
function findContainingGroup(point, nodes, exclude = []) {
  for (let i = nodes.length - 1; i >= 0; i--) {
    const n = nodes[i];
    if (exclude.includes(n.id)) continue;
    if (n.type !== "group") continue;
    if (n.data && typeof n.data === "object" && n.data.collapsed) {
      continue;
    }
    const w = n.width ?? DEFAULT_NODE_WIDTH;
    const h = effectiveHeight(n);
    if (point.x >= n.position.x && point.y >= n.position.y && point.x <= n.position.x + w && point.y <= n.position.y + h) {
      return n;
    }
  }
  return void 0;
}

// src/workflow/components/FlowEdge/FlowEdge.tsx
import {
  memo,
  useEffect,
  useId,
  useRef,
  useState
} from "react";

// src/workflow/components/Handle/handleRegistry.ts
import { createContext as createContext2, useContext as useContext2 } from "react";
var HandleRegistryContext = createContext2(null);
function useHandleRegistry() {
  const r = useContext2(HandleRegistryContext);
  if (!r) {
    throw new Error("[@octaviaflow/core/workflow] Handle must be used inside <FlowCanvas>.");
  }
  return r;
}
function createHandleRegistry() {
  const map = /* @__PURE__ */ new Map();
  const key = (n, t, h) => `${n}::${t}::${h}`;
  const listeners = /* @__PURE__ */ new Set();
  const notify = () => {
    for (const l of listeners) l();
  };
  return {
    register(d) {
      map.set(key(d.nodeId, d.type, d.handleId), d);
      notify();
      return () => {
        const k = key(d.nodeId, d.type, d.handleId);
        if (map.get(k) === d) {
          map.delete(k);
          notify();
        }
      };
    },
    resolve(nodeId, type, handleId) {
      return map.get(key(nodeId, type, handleId));
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => {
        listeners.delete(listener);
      };
    }
  };
}

// src/workflow/components/FlowEdge/FlowEdge.tsx
import { jsx, jsxs } from "react/jsx-runtime";
function FlowEdgeImpl({
  edge,
  nodes,
  onSelect,
  onDelete,
  onLabelChange,
  showDelete = true,
  curvature = 0.25,
  borderRadius = 8
}) {
  const uid = useId();
  const markerId = `ods-flow-edge-arrow-${uid.replace(/:/g, "")}`;
  const registry = useHandleRegistry();
  const [hovered, setHovered] = useState(false);
  const [editing, setEditing] = useState(false);
  const [draftLabel, setDraftLabel] = useState(edge.label ?? "");
  const inputRef = useRef(null);
  useEffect(() => {
    if (!editing) setDraftLabel(edge.label ?? "");
  }, [edge.label, editing]);
  const sourceNode = nodes.find((n) => n.id === edge.source);
  const targetNode = nodes.find((n) => n.id === edge.target);
  if (!sourceNode || !targetNode) return null;
  const sourceHandleId = edge.sourceHandle ?? "default";
  const targetHandleId = edge.targetHandle ?? "default";
  const sourceDesc = registry.resolve(sourceNode.id, "source", sourceHandleId);
  const targetDesc = registry.resolve(targetNode.id, "target", targetHandleId);
  const sourceSide = sourceDesc?.side ?? sourceNode.sourcePosition ?? "bottom";
  const targetSide = targetDesc?.side ?? targetNode.targetPosition ?? "top";
  const sourceIndex = sourceDesc?.index ?? 0;
  const sourceTotal = sourceDesc?.total ?? 1;
  const targetIndex = targetDesc?.index ?? 0;
  const targetTotal = targetDesc?.total ?? 1;
  const rawStart = handleCentre(sourceNode, sourceSide, sourceIndex, sourceTotal);
  const rawEnd = handleCentre(targetNode, targetSide, targetIndex, targetTotal);
  const HANDLE_GAP = 8;
  const start = offsetAlongSide2(rawStart, sourceSide, HANDLE_GAP);
  const end = offsetAlongSide2(rawEnd, targetSide, HANDLE_GAP);
  const routing = edge.routing ?? "bezier";
  const { d, midX, midY } = buildEdgePath(routing, start, sourceSide, end, targetSide, {
    curvature,
    borderRadius
  });
  const { d: hitD } = buildEdgePath(routing, rawStart, sourceSide, rawEnd, targetSide, {
    curvature,
    borderRadius
  });
  const type = edge.type ?? "default";
  const isHot = hovered || edge.selected;
  const stroke = type === "error" ? "var(--ods-status-failed, #dc2626)" : isHot ? "var(--ods-accent, #4f46e5)" : "var(--ods-border-strong, #6b7280)";
  const typeDash = type === "conditional" ? "6 4" : type === "loop" ? "4 4" : void 0;
  const dash = edge.style?.strokeDasharray ?? typeDash;
  const strokeWidth = edge.style?.strokeWidth ?? (isHot ? 2 : 1.4);
  const animateDash = edge.animated === true;
  const label = edge.label;
  const hasDelete = showDelete && (isHot || edge.selected) && !!onDelete;
  const showChrome = !!label || editing || hasDelete;
  const commitLabel = (next) => {
    setEditing(false);
    if (next !== edge.label) onLabelChange?.(edge.id, next);
  };
  const onLabelKey = (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      commitLabel(draftLabel);
    } else if (e.key === "Escape") {
      e.preventDefault();
      setEditing(false);
      setDraftLabel(edge.label ?? "");
    }
  };
  return /* @__PURE__ */ jsxs(
    "g",
    {
      className: cn(
        "ods-flow-edge-v2",
        `ods-flow-edge-v2--${type}`,
        `ods-flow-edge-v2--routing-${routing}`,
        edge.selected && "ods-flow-edge-v2--selected",
        hovered && "ods-flow-edge-v2--hovered",
        edge.className
      ),
      "data-edge-id": edge.id,
      "data-edge-routing": routing,
      onMouseEnter: () => setHovered(true),
      onMouseLeave: () => setHovered(false),
      onClick: (e) => {
        e.stopPropagation();
        onSelect?.(edge.id);
      },
      children: [
        /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
          "marker",
          {
            id: markerId,
            viewBox: "0 0 10 10",
            refX: "9",
            refY: "5",
            markerWidth: "7",
            markerHeight: "7",
            orient: "auto-start-reverse",
            children: /* @__PURE__ */ jsx("path", { d: "M0 0L10 5L0 10z", fill: stroke })
          }
        ) }),
        /* @__PURE__ */ jsx("path", { d: hitD, stroke: "transparent", strokeWidth: 20, fill: "none" }),
        /* @__PURE__ */ jsx(
          "path",
          {
            d,
            stroke,
            strokeWidth,
            strokeDasharray: dash,
            fill: "none",
            markerEnd: `url(#${markerId})`,
            style: animateDash ? {
              strokeDasharray: dash ?? "6 4",
              animation: "ods-flow-edge-flow 0.6s linear infinite"
            } : void 0
          }
        ),
        showChrome && /* @__PURE__ */ jsx(
          "foreignObject",
          {
            x: midX - 140,
            y: midY - 16,
            width: 280,
            height: 32,
            style: { overflow: "visible", pointerEvents: "none" },
            children: /* @__PURE__ */ jsxs("div", { className: "ods-flow-edge-v2__chrome", children: [
              (label || editing) && (editing ? /* @__PURE__ */ jsx(
                "input",
                {
                  ref: inputRef,
                  className: "ods-flow-edge-v2__label-input",
                  value: draftLabel,
                  autoFocus: true,
                  onChange: (e) => setDraftLabel(e.target.value),
                  onBlur: () => commitLabel(draftLabel),
                  onKeyDown: onLabelKey,
                  onClick: (e) => e.stopPropagation(),
                  onMouseDown: (e) => e.stopPropagation(),
                  "aria-label": "Edit edge label"
                }
              ) : /* @__PURE__ */ jsx(
                "button",
                {
                  type: "button",
                  className: "ods-flow-edge-v2__label-chip",
                  onDoubleClick: (e) => {
                    if (!onLabelChange) return;
                    e.stopPropagation();
                    setDraftLabel(edge.label ?? "");
                    setEditing(true);
                  },
                  onClick: (e) => {
                    e.stopPropagation();
                    onSelect?.(edge.id);
                  },
                  title: onLabelChange ? "Double-click to edit" : void 0,
                  tabIndex: onLabelChange ? 0 : -1,
                  children: /* @__PURE__ */ jsx("span", { children: label })
                }
              )),
              hasDelete && /* @__PURE__ */ jsx(
                "button",
                {
                  type: "button",
                  className: "ods-flow-edge-v2__delete-btn",
                  onClick: (e) => {
                    e.stopPropagation();
                    onDelete?.(edge.id);
                  },
                  "aria-label": "Delete edge",
                  children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 10 10", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
                    "path",
                    {
                      d: "M2 2l6 6M8 2l-6 6",
                      stroke: "currentColor",
                      strokeWidth: 1.5,
                      strokeLinecap: "round"
                    }
                  ) })
                }
              )
            ] })
          }
        )
      ]
    }
  );
}
function offsetAlongSide2(p, side, gap) {
  switch (side) {
    case "top":
      return { x: p.x, y: p.y - gap };
    case "bottom":
      return { x: p.x, y: p.y + gap };
    case "left":
      return { x: p.x - gap, y: p.y };
    case "right":
      return { x: p.x + gap, y: p.y };
  }
}
var FlowEdge = memo(FlowEdgeImpl, (prev, next) => {
  if (prev.edge !== next.edge) return false;
  if (prev.onSelect !== next.onSelect || prev.onDelete !== next.onDelete) return false;
  if (prev.onLabelChange !== next.onLabelChange) return false;
  if (prev.showDelete !== next.showDelete) return false;
  if (prev.curvature !== next.curvature || prev.borderRadius !== next.borderRadius) return false;
  if (prev.handleVersion !== next.handleVersion) return false;
  const ps = prev.nodes.find((n) => n.id === prev.edge.source);
  const pt = prev.nodes.find((n) => n.id === prev.edge.target);
  const ns = next.nodes.find((n) => n.id === next.edge.source);
  const nt = next.nodes.find((n) => n.id === next.edge.target);
  if (ps !== ns || pt !== nt) return false;
  return true;
});

// src/workflow/components/FlowNode/FlowNodeContext.tsx
import { createContext as createContext3, useContext as useContext3 } from "react";
var FlowNodeContext = createContext3(null);
function useFlowNodeContext() {
  const v = useContext3(FlowNodeContext);
  if (!v) {
    throw new Error(
      "[@octaviaflow/core/workflow] Handle / NodeToolbar must be inside a node renderer."
    );
  }
  return v;
}

// src/workflow/components/FlowNode/FlowNode.tsx
import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";

// src/workflow/components/FlowCanvas/FlowCanvasContext.tsx
import { createContext as createContext4, useContext as useContext4 } from "react";
var FlowDispatchContext = createContext4(null);
function useFlowDispatch() {
  const dispatch = useContext4(FlowDispatchContext);
  if (!dispatch) {
    throw new Error(
      "[@octaviaflow/core/workflow] useFlowDispatch must be called inside <FlowCanvas>."
    );
  }
  return dispatch;
}
var FlowNodeBridgeContext = createContext4(null);
function useFlowNodeBridge() {
  const b = useContext4(FlowNodeBridgeContext);
  if (!b) {
    throw new Error("[@octaviaflow/core/workflow] FlowNode must be a child of <FlowCanvas>.");
  }
  return b;
}

// src/workflow/components/FlowNode/FlowNode.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
function FlowNode({
  node,
  selected,
  dragging,
  isConnecting,
  Kind,
  // Match the BaseNode body's fixed 368 px width so the wrapper bbox and
  // the rendered card line up on the first paint — before the
  // ResizeObserver has had a chance to write back the measured size.
  // Container kinds (group / forEach) opt out by setting `node.width`.
  defaultWidth = 368
}) {
  const bridge = useFlowNodeBridge();
  const wrapperRef = useRef2(null);
  useEffect2(() => {
    const el = wrapperRef.current;
    if (!el || typeof ResizeObserver === "undefined") return;
    if (node.type === "group" || node.type === "forEach") return;
    const ro = new ResizeObserver(() => {
      bridge.reportDimensions(node.id, el.offsetWidth, el.offsetHeight);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, [bridge, node.id, node.type]);
  const ctx = useMemo(
    () => ({ id: node.id, node, selected }),
    [node, selected]
  );
  const downPosRef = useRef2(null);
  const draggedRef = useRef2(false);
  const CLICK_VS_DRAG_PX = 5;
  const handlePointerDown = (e) => {
    if (e.target.closest("[data-handle-id]")) return;
    if (e.target.closest("[data-flow-no-drag='true']")) return;
    e.stopPropagation();
    downPosRef.current = { x: e.clientX, y: e.clientY };
    draggedRef.current = false;
    bridge.beginNodeDrag(node.id, e.pointerId, e.clientX, e.clientY, e.altKey);
  };
  const handlePointerMove = (e) => {
    const start = downPosRef.current;
    if (!start || draggedRef.current) return;
    if (Math.abs(e.clientX - start.x) > CLICK_VS_DRAG_PX || Math.abs(e.clientY - start.y) > CLICK_VS_DRAG_PX) {
      draggedRef.current = true;
    }
  };
  const handleClick = (e) => {
    if (e.target.closest("[data-handle-id]")) return;
    e.stopPropagation();
    if (draggedRef.current) {
      draggedRef.current = false;
      downPosRef.current = null;
      return;
    }
    downPosRef.current = null;
    bridge.selectNode(node.id, e.metaKey || e.ctrlKey || e.shiftKey);
    bridge.notifyNodeClick(node.id);
  };
  return /* @__PURE__ */ jsx2(
    "div",
    {
      ref: wrapperRef,
      "data-node-id": node.id,
      "data-node-type": node.type,
      "data-node-selected": selected ? "true" : "false",
      className: cn(
        "ods-flow-node-v2",
        `ods-flow-node-v2--kind-${node.type}`,
        selected && "ods-flow-node-v2--selected",
        dragging && "ods-flow-node-v2--dragging",
        isConnecting && "ods-flow-node-v2--connecting",
        node.className
      ),
      style: {
        position: "absolute",
        left: node.position.x,
        top: node.position.y,
        width: node.width ?? defaultWidth,
        // Z-stack (per architecture doc): groups (20) sit below normal
        // nodes (30) so children render visually on top of their parent
        // frame. Selection raises by 10. Consumer override wins.
        zIndex: node.zIndex ?? (selected ? node.type === "group" ? 35 : 40 : node.type === "group" ? 20 : 30),
        ...node.style
      },
      onPointerDown: handlePointerDown,
      onPointerMove: handlePointerMove,
      onClick: handleClick,
      children: /* @__PURE__ */ jsx2(FlowNodeContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx2(Kind, { node, selected, dragging, isConnecting }) })
    }
  );
}

// src/workflow/components/FlowNode/nodeKinds.ts
function buildNodeKindRegistry(defaults, overrides) {
  return Object.freeze({ ...defaults, ...overrides ?? {} });
}

// src/workflow/components/Handle/Handle.tsx
import { useEffect as useEffect3, useRef as useRef3 } from "react";
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var DEFAULT_HANDLE_ID = "default";
function Handle({
  type,
  position,
  id = DEFAULT_HANDLE_ID,
  isConnectable = true,
  isConnectableStart,
  isConnectableEnd,
  index = 0,
  total = 1,
  label,
  className,
  style
}) {
  const registry = useHandleRegistry();
  const node = useFlowNodeContext();
  const dispatch = useFlowDispatch();
  const ref = useRef3(null);
  const canStart = isConnectableStart ?? isConnectable;
  const canEnd = isConnectableEnd ?? isConnectable;
  useEffect3(() => {
    const dispose = registry.register({
      nodeId: node.id,
      handleId: id,
      type,
      side: position,
      index,
      total
    });
    return dispose;
  }, [registry, node.id, id, type, position, index, total]);
  const handlePointerDown = (e) => {
    if (!canStart) return;
    e.stopPropagation();
    e.preventDefault();
    dispatch({
      type: "connection/start",
      nodeId: node.id,
      handleId: id,
      handleType: type,
      pointerId: e.pointerId,
      clientX: e.clientX,
      clientY: e.clientY
    });
  };
  return /* @__PURE__ */ jsxs2(
    "div",
    {
      ref,
      "data-handle-id": id,
      "data-handle-node-id": node.id,
      "data-handle-type": type,
      "data-handle-side": position,
      "data-handle-connectable-end": canEnd ? "true" : "false",
      className: cn(
        "ods-flow-handle",
        `ods-flow-handle--${type}`,
        `ods-flow-handle--side-${position}`,
        !isConnectable && "ods-flow-handle--disabled",
        className
      ),
      style: {
        ...handleSideStyle(position, index, total),
        ...style
      },
      onPointerDown: handlePointerDown,
      children: [
        /* @__PURE__ */ jsx3("div", { className: "ods-flow-handle__dot" }),
        label && /* @__PURE__ */ jsx3("span", { className: "ods-flow-handle__label", children: label })
      ]
    }
  );
}
function handleSideStyle(side, index, total) {
  const ratio = (index + 1) / (total + 1) * 100;
  switch (side) {
    case "top":
      return { position: "absolute", top: -6, left: `${ratio}%`, transform: "translateX(-50%)" };
    case "bottom":
      return { position: "absolute", bottom: -6, left: `${ratio}%`, transform: "translateX(-50%)" };
    case "left":
      return { position: "absolute", left: -6, top: `${ratio}%`, transform: "translateY(-50%)" };
    case "right":
      return { position: "absolute", right: -6, top: `${ratio}%`, transform: "translateY(-50%)" };
  }
}

// src/workflow/components/kinds/BaseNode.tsx
import { TrashCanIcon } from "@octaviaflow/icons";
import { useContext as useContext5 } from "react";
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
function BaseNode({
  kind,
  kindIcon,
  icon,
  title,
  chip,
  description,
  accent = "green",
  status,
  onDelete,
  footer,
  className,
  children
}) {
  const ctx = useContext5(FlowNodeContext);
  const bridge = useContext5(FlowNodeBridgeContext);
  const deleteHandler = onDelete === false ? void 0 : onDelete ?? (ctx && bridge ? () => bridge.deleteNode(ctx.id) : void 0);
  return /* @__PURE__ */ jsxs3(
    "div",
    {
      className: cn(
        "ods-flow-base-node",
        `ods-flow-base-node--accent-${accent}`,
        status && `ods-flow-base-node--status-${status}`,
        className
      ),
      children: [
        /* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__pill", children: [
          kindIcon && /* @__PURE__ */ jsx4("span", { className: "ods-flow-base-node__pill-icon", "aria-hidden": "true", children: kindIcon }),
          /* @__PURE__ */ jsx4("span", { className: "ods-flow-base-node__pill-label", children: kind })
        ] }),
        status && status !== "idle" && /* @__PURE__ */ jsx4(
          "span",
          {
            className: cn("ods-flow-base-node__status", `ods-flow-base-node__status--${status}`),
            "aria-hidden": "true"
          }
        ),
        /* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__body", children: [
          /* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__content", children: [
            /* @__PURE__ */ jsx4("div", { className: "ods-flow-base-node__bubble", "aria-hidden": "true", children: icon }),
            /* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__content-text", children: [
              /* @__PURE__ */ jsx4("div", { className: "ods-flow-base-node__content-title", children: title }),
              (chip !== void 0 || description !== void 0) && /* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__content-info", children: [
                chip !== void 0 && /* @__PURE__ */ jsx4("span", { className: "ods-flow-base-node__chip", children: chip }),
                description !== void 0 && /* @__PURE__ */ jsx4("span", { className: "ods-flow-base-node__description", children: description })
              ] })
            ] })
          ] }),
          footer && /* @__PURE__ */ jsx4("div", { className: "ods-flow-base-node__footer", children: footer }),
          deleteHandler && /* @__PURE__ */ jsx4(
            "button",
            {
              type: "button",
              className: "ods-flow-base-node__delete",
              onClick: (e) => {
                e.stopPropagation();
                deleteHandler();
              },
              "aria-label": "Delete node",
              "data-flow-no-drag": "true",
              title: "Delete node",
              children: /* @__PURE__ */ jsx4(TrashCanIcon, { size: 16, "aria-hidden": true })
            }
          )
        ] }),
        children
      ]
    }
  );
}

// src/workflow/components/kinds/index.tsx
import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
var ActionNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsxs4(
    BaseNode,
    {
      kind: d.kind ?? "ACTION",
      icon: d.icon,
      title: d.title ?? "Action",
      chip: d.chip ?? d.badge,
      description: d.description ?? d.subtitle,
      status: d.status,
      accent: "green",
      children: [
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" }),
        /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom" })
      ]
    }
  );
};
var TriggerNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsx5(
    BaseNode,
    {
      kind: d.kind ?? "TRIGGER",
      icon: d.icon,
      title: d.title ?? "Trigger",
      chip: d.chip,
      description: d.description ?? "Manually triggered",
      status: d.status,
      accent: "green",
      children: /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom" })
    }
  );
};
var ConditionNode = ({
  node
}) => {
  const d = node.data ?? {};
  const branches = d.branches ?? [
    { id: "true", label: "true" },
    { id: "false", label: "false" }
  ];
  return /* @__PURE__ */ jsxs4(
    BaseNode,
    {
      kind: d.kind ?? "CONDITION",
      icon: d.icon,
      title: d.title ?? "Condition",
      chip: d.chip ?? d.badge,
      description: d.description ?? d.subtitle,
      status: d.status,
      accent: "amber",
      children: [
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" }),
        branches.map((b, i) => /* @__PURE__ */ jsx5(
          Handle,
          {
            type: "source",
            position: "bottom",
            id: b.id,
            index: i,
            total: branches.length,
            label: b.label
          },
          b.id
        ))
      ]
    }
  );
};
var GroupNode = ({
  node
}) => {
  const d = node.data ?? {};
  const collapsed = !!d.collapsed;
  const hiddenCount = d.hiddenCount;
  const bridge = useFlowNodeBridge();
  const onChevronClick = (e) => {
    e.stopPropagation();
    bridge.toggleNodeCollapse(node.id);
  };
  return /* @__PURE__ */ jsxs4(
    "div",
    {
      className: "ods-flow-group",
      "data-collapsed": collapsed ? "true" : "false",
      style: {
        width: node.width ?? 360,
        height: collapsed ? 36 : node.height ?? 200
      },
      children: [
        /* @__PURE__ */ jsxs4("div", { className: "ods-flow-group__header", "data-flow-no-drag": "false", children: [
          /* @__PURE__ */ jsx5(
            "button",
            {
              type: "button",
              className: "ods-flow-group__chevron",
              "data-flow-no-drag": "true",
              "aria-label": collapsed ? "Expand group" : "Collapse group",
              "aria-expanded": !collapsed,
              onClick: onChevronClick,
              onPointerDown: (e) => e.stopPropagation(),
              children: collapsed ? "\u25B8" : "\u25BE"
            }
          ),
          /* @__PURE__ */ jsx5("span", { className: "ods-flow-group__title", children: d.title ?? "Group" }),
          d.subtitle && /* @__PURE__ */ jsx5("span", { className: "ods-flow-group__subtitle", children: d.subtitle }),
          collapsed && hiddenCount !== void 0 && hiddenCount > 0 && /* @__PURE__ */ jsxs4("span", { className: "ods-flow-group__count", "aria-label": `${hiddenCount} hidden steps`, children: [
            hiddenCount,
            " steps"
          ] })
        ] }),
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top", id: "__group_in" }),
        /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom", id: "__group_out" })
      ]
    }
  );
};
var ForEachNode = ({
  node
}) => {
  const d = node.data ?? {};
  const iteratorExpr = d.iterator ?? d.description ?? "items[]";
  const collapsed = !!d.collapsed;
  const hiddenCount = d.hiddenCount;
  const bridge = useFlowNodeBridge();
  const onChevronClick = (e) => {
    e.stopPropagation();
    bridge.toggleNodeCollapse(node.id);
  };
  return /* @__PURE__ */ jsxs4(
    "div",
    {
      className: "ods-flow-foreach",
      "data-collapsed": collapsed ? "true" : "false",
      style: {
        width: node.width ?? 420,
        height: collapsed ? 40 : node.height ?? 260
      },
      children: [
        /* @__PURE__ */ jsxs4("div", { className: "ods-flow-foreach__header", children: [
          /* @__PURE__ */ jsx5(
            "button",
            {
              type: "button",
              className: "ods-flow-foreach__chevron",
              "data-flow-no-drag": "true",
              "aria-label": collapsed ? "Expand iterator" : "Collapse iterator",
              "aria-expanded": !collapsed,
              onClick: onChevronClick,
              onPointerDown: (e) => e.stopPropagation(),
              children: collapsed ? "\u25B8" : "\u25BE"
            }
          ),
          /* @__PURE__ */ jsx5("span", { className: "ods-flow-foreach__icon", "aria-hidden": "true", children: "\u21BB" }),
          /* @__PURE__ */ jsx5("span", { className: "ods-flow-foreach__title", children: d.title ?? "For each" }),
          /* @__PURE__ */ jsx5("code", { className: "ods-flow-foreach__iterator", children: iteratorExpr }),
          collapsed && hiddenCount !== void 0 && hiddenCount > 0 && /* @__PURE__ */ jsxs4("span", { className: "ods-flow-foreach__count", "aria-label": `${hiddenCount} hidden steps`, children: [
            hiddenCount,
            " steps"
          ] })
        ] }),
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top", id: "__group_in" }),
        /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom", id: "__group_out" }),
        !collapsed && // Expanded mode — full 3-handle iterator surface (in / item / done).
        // `in` shares the top edge with `__group_in`; consumers wiring an
        // explicit `targetHandle: "in"` get the labelled, visible variant.
        /* @__PURE__ */ jsxs4(Fragment, { children: [
          /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top", id: "in", label: "in" }),
          /* @__PURE__ */ jsx5(Handle, { type: "source", position: "right", id: "item", label: "item" }),
          /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom", id: "done", label: "done" })
        ] })
      ]
    }
  );
};
var OutputNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsx5(
    BaseNode,
    {
      kind: d.kind ?? "OUTPUT",
      icon: d.icon,
      title: d.title ?? "Output",
      chip: d.chip ?? d.badge,
      description: d.description ?? d.subtitle,
      status: d.status,
      accent: "green",
      children: /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" })
    }
  );
};
var ErrorNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsxs4(
    BaseNode,
    {
      kind: d.kind ?? "ERROR",
      icon: d.icon,
      title: d.title ?? "On error",
      chip: d.chip ?? d.badge,
      description: d.description ?? d.subtitle,
      status: d.status ?? "error",
      accent: "red",
      children: [
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" }),
        /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom" })
      ]
    }
  );
};
var WaitNode = ({
  node
}) => {
  const d = node.data ?? {};
  const waitMs = d.waitMs;
  const durationChip = waitMs ? `${Math.round(waitMs / 100) / 10}s` : void 0;
  return /* @__PURE__ */ jsxs4(
    BaseNode,
    {
      kind: d.kind ?? "WAIT",
      icon: d.icon,
      title: d.title ?? "Wait",
      chip: d.chip ?? durationChip,
      description: d.description ?? (durationChip ? "Pause execution" : void 0),
      status: d.status,
      accent: "violet",
      children: [
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" }),
        /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom" })
      ]
    }
  );
};
var ParallelNode = ({
  node
}) => {
  const d = node.data ?? {};
  const branches = d.branches ?? [
    { id: "a", label: "a" },
    { id: "b", label: "b" }
  ];
  return /* @__PURE__ */ jsxs4(
    BaseNode,
    {
      kind: d.kind ?? "PARALLEL",
      icon: d.icon,
      title: d.title ?? "Parallel",
      chip: d.chip ?? `${branches.length}\xD7`,
      description: d.description ?? "Fan-out branches",
      status: d.status,
      accent: "blue",
      children: [
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" }),
        branches.map((b, i) => /* @__PURE__ */ jsx5(
          Handle,
          {
            type: "source",
            position: "bottom",
            id: b.id,
            index: i,
            total: branches.length,
            label: b.label
          },
          b.id
        ))
      ]
    }
  );
};
var StickyNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsxs4(
    "div",
    {
      className: "ods-flow-sticky",
      style: {
        width: node.width ?? 240,
        minHeight: node.height ?? 120
      },
      children: [
        d.title && /* @__PURE__ */ jsx5("div", { className: "ods-flow-sticky__title", children: d.title }),
        d.description && /* @__PURE__ */ jsx5("div", { className: "ods-flow-sticky__body", children: d.description })
      ]
    }
  );
};
var WebhookNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsx5(
    BaseNode,
    {
      kind: d.kind ?? "WEBHOOK",
      icon: d.icon,
      title: d.title ?? "Webhook",
      chip: d.chip ?? d.method ?? "POST",
      description: d.description ?? d.path ?? "/hooks/incoming",
      status: d.status,
      accent: "blue",
      children: /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom" })
    }
  );
};
var HttpRequestNode = ({
  node
}) => {
  const d = node.data ?? {};
  return /* @__PURE__ */ jsxs4(
    BaseNode,
    {
      kind: d.kind ?? "HTTP",
      icon: d.icon,
      title: d.title ?? "HTTP Request",
      chip: d.chip ?? d.method ?? "GET",
      description: d.description ?? d.url ?? "Call an API",
      status: d.status,
      accent: "blue",
      children: [
        /* @__PURE__ */ jsx5(Handle, { type: "target", position: "top" }),
        /* @__PURE__ */ jsx5(Handle, { type: "source", position: "bottom" })
      ]
    }
  );
};
var DEFAULT_NODE_KINDS = {
  action: ActionNode,
  trigger: TriggerNode,
  condition: ConditionNode,
  group: GroupNode,
  forEach: ForEachNode,
  output: OutputNode,
  error: ErrorNode,
  wait: WaitNode,
  parallel: ParallelNode,
  sticky: StickyNode,
  webhook: WebhookNode,
  httpRequest: HttpRequestNode
};

// src/workflow/components/FlowCanvas/FlowCanvas.tsx
import {
  memo as memo2,
  useCallback,
  useEffect as useEffect4,
  useMemo as useMemo2,
  useRef as useRef4,
  useState as useState2
} from "react";

// src/workflow/store/context.ts
import { createContext as createContext5, useContext as useContext6 } from "react";
var FlowStoreContext = createContext5(null);
function useFlowStore() {
  const store = useContext6(FlowStoreContext);
  if (!store) {
    throw new Error(
      "[@octaviaflow/core/workflow] useFlowStore must be called inside a <FlowCanvas>."
    );
  }
  return store;
}

// src/workflow/store/createFlowStore.ts
var DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: 1 };
function createFlowStore({
  initialNodes = [],
  initialEdges = [],
  initialViewport = DEFAULT_VIEWPORT
} = {}) {
  let snapshot = {
    nodes: initialNodes,
    edges: initialEdges,
    viewport: initialViewport,
    selectedNodeIds: EMPTY_SET,
    selectedEdgeIds: EMPTY_SET,
    connection: null
  };
  const listeners = /* @__PURE__ */ new Set();
  const emit = () => {
    for (const l of listeners) l();
  };
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => {
      listeners.delete(listener);
    };
  };
  const replace = (patch) => {
    snapshot = { ...snapshot, ...patch };
    emit();
  };
  return {
    subscribe,
    getSnapshot: () => snapshot,
    setNodes(nodes) {
      if (nodes === snapshot.nodes) return;
      replace({ nodes });
    },
    setEdges(edges) {
      if (edges === snapshot.edges) return;
      replace({ edges });
    },
    setViewport(viewport) {
      if (viewport.x === snapshot.viewport.x && viewport.y === snapshot.viewport.y && viewport.zoom === snapshot.viewport.zoom) {
        return;
      }
      replace({ viewport });
    },
    setSelection(selectedNodeIds, selectedEdgeIds) {
      replace({ selectedNodeIds, selectedEdgeIds });
    },
    setConnection(connection) {
      if (connection === snapshot.connection) return;
      replace({ connection });
    }
  };
}
var EMPTY_SET = /* @__PURE__ */ new Set();

// src/workflow/utils/collision.ts
function resolveNodeCollisions(node, proposed, others, opts) {
  const { gap, maxIterations = 8, exclude, scopeToSiblings = true } = opts;
  if (gap < 0 || others.length === 0) return proposed;
  const w = node.width ?? DEFAULT_NODE_WIDTH;
  const h = effectiveHeight(node);
  const parentId = node.parentId;
  const candidates = others.filter((o) => {
    if (o.id === node.id) return false;
    if (o.hidden) return false;
    if (exclude?.has(o.id)) return false;
    if (scopeToSiblings && (o.parentId ?? void 0) !== (parentId ?? void 0)) {
      return false;
    }
    return true;
  });
  if (candidates.length === 0) return proposed;
  let cur = proposed;
  for (let iter = 0; iter < maxIterations; iter++) {
    let bestOverlap = Number.POSITIVE_INFINITY;
    let bestPushX = 0;
    let bestPushY = 0;
    let hit = false;
    const aLeft = cur.x - gap;
    const aRight = cur.x + w + gap;
    const aTop = cur.y - gap;
    const aBottom = cur.y + h + gap;
    for (const o of candidates) {
      const ow = o.width ?? DEFAULT_NODE_WIDTH;
      const oh = effectiveHeight(o);
      const bLeft = o.position.x;
      const bRight = o.position.x + ow;
      const bTop = o.position.y;
      const bBottom = o.position.y + oh;
      const overlapX = Math.min(aRight, bRight) - Math.max(aLeft, bLeft);
      const overlapY = Math.min(aBottom, bBottom) - Math.max(aTop, bTop);
      if (overlapX <= 0 || overlapY <= 0) continue;
      let pushX = 0;
      let pushY = 0;
      let overlap;
      if (overlapX < overlapY) {
        const aCx = cur.x + w / 2;
        const bCx = o.position.x + ow / 2;
        pushX = aCx < bCx ? -overlapX : overlapX;
        overlap = overlapX;
      } else {
        const aCy = cur.y + h / 2;
        const bCy = o.position.y + oh / 2;
        pushY = aCy < bCy ? -overlapY : overlapY;
        overlap = overlapY;
      }
      if (overlap < bestOverlap) {
        bestOverlap = overlap;
        bestPushX = pushX;
        bestPushY = pushY;
        hit = true;
      }
    }
    if (!hit) break;
    cur = { x: cur.x + bestPushX, y: cur.y + bestPushY };
  }
  return cur;
}

// src/workflow/components/FlowCanvas/FlowCanvas.tsx
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
var DEFAULT_VIEWPORT2 = { x: 0, y: 0, zoom: 1 };
function FlowCanvas({
  nodes,
  edges,
  onNodesChange,
  onEdgesChange,
  viewport: controlledViewport,
  defaultViewport = DEFAULT_VIEWPORT2,
  onViewportChange,
  minZoom = 0.25,
  maxZoom = 2,
  nodeKinds,
  onConnect,
  onConnectStart,
  onConnectEnd,
  isValidConnection,
  onSelectionChange,
  onPaneClick,
  onNodeClick,
  onEdgeClick,
  onEdgeLabelChange,
  onInit,
  onBeforeDelete,
  onNodeContextMenu,
  onEdgeContextMenu,
  onPaneContextMenu,
  nodesDraggable = true,
  nodesConnectable = true,
  panOnDrag: panOnDragProp,
  zoomOnScroll: zoomOnScrollProp,
  preset = "mouse",
  paneClickDistance = 4,
  paneClickClearsSelection = true,
  background = "dots",
  gridSize = 20,
  snapToGrid = false,
  nodeCollisionGap = -1,
  subflowCollisionGap,
  height = "100%",
  width = "100%",
  className,
  style,
  children,
  emptyState
}) {
  const presetDefaults = {
    mouse: { panOnDrag: true, zoomOnScroll: true },
    trackpad: { panOnDrag: true, zoomOnScroll: true },
    touch: { panOnDrag: false, zoomOnScroll: true }
  };
  const panOnDrag = panOnDragProp ?? presetDefaults[preset].panOnDrag;
  const zoomOnScroll = zoomOnScrollProp ?? presetDefaults[preset].zoomOnScroll;
  const store = useState2(
    () => createFlowStore({
      initialNodes: nodes,
      initialEdges: edges,
      initialViewport: controlledViewport ?? defaultViewport
    })
  )[0];
  const handleRegistry = useState2(() => createHandleRegistry())[0];
  const [handleVersion, setHandleVersion] = useState2(0);
  useEffect4(() => {
    const unsub = handleRegistry.subscribe(() => {
      setHandleVersion((v) => v + 1);
    });
    return unsub;
  }, [handleRegistry]);
  const kinds = useMemo2(() => buildNodeKindRegistry(DEFAULT_NODE_KINDS, nodeKinds), [nodeKinds]);
  const containerRef = useRef4(null);
  useEffect4(() => store.setNodes(nodes), [store, nodes]);
  useEffect4(() => store.setEdges(edges), [store, edges]);
  const [uncontrolledVp, setUncontrolledVp] = useState2(controlledViewport ?? defaultViewport);
  const viewport = controlledViewport ?? uncontrolledVp;
  useEffect4(() => store.setViewport(viewport), [store, viewport]);
  const setViewport = useCallback(
    (next) => {
      if (controlledViewport === void 0) setUncontrolledVp(next);
      onViewportChange?.(next);
    },
    [controlledViewport, onViewportChange]
  );
  const selectedNodeIds = useMemo2(() => {
    const s = /* @__PURE__ */ new Set();
    for (const n of nodes) if (n.selected) s.add(n.id);
    return s;
  }, [nodes]);
  const selectedEdgeIds = useMemo2(() => {
    const s = /* @__PURE__ */ new Set();
    for (const e of edges) if (e.selected) s.add(e.id);
    return s;
  }, [edges]);
  useEffect4(() => {
    store.setSelection(selectedNodeIds, selectedEdgeIds);
    if (onSelectionChange) {
      onSelectionChange({
        nodes: nodes.filter((n) => selectedNodeIds.has(n.id)),
        edges: edges.filter((e) => selectedEdgeIds.has(e.id))
      });
    }
  }, [store, selectedNodeIds, selectedEdgeIds, nodes, edges, onSelectionChange]);
  const selectNode = useCallback(
    (id, additive) => {
      const next = [];
      const nextEdges = [];
      if (!additive) {
        for (const n of nodes) {
          if (n.selected && n.id !== id) next.push(change.node.select(n.id, false));
        }
        for (const e of edges) {
          if (e.selected) nextEdges.push(change.edge.select(e.id, false));
        }
      }
      const isSelected = selectedNodeIds.has(id);
      if (additive && isSelected) {
        next.push(change.node.select(id, false));
      } else if (!isSelected) {
        next.push(change.node.select(id, true));
      }
      if (next.length) onNodesChange?.(next);
      if (nextEdges.length) onEdgesChange?.(nextEdges);
    },
    [nodes, edges, selectedNodeIds, onNodesChange, onEdgesChange]
  );
  const notifyNodeClick = useCallback(
    (id) => {
      const node = nodes.find((n) => n.id === id);
      if (node) onNodeClick?.(node);
    },
    [nodes, onNodeClick]
  );
  const selectEdge = useCallback(
    (id, additive) => {
      const next = [];
      const nextNodes = [];
      if (!additive) {
        for (const e of edges) {
          if (e.selected && e.id !== id) next.push(change.edge.select(e.id, false));
        }
        for (const n of nodes) {
          if (n.selected) nextNodes.push(change.node.select(n.id, false));
        }
      }
      const isSelected = selectedEdgeIds.has(id);
      if (additive && isSelected) {
        next.push(change.edge.select(id, false));
      } else if (!isSelected) {
        next.push(change.edge.select(id, true));
      }
      if (next.length) onEdgesChange?.(next);
      if (nextNodes.length) onNodesChange?.(nextNodes);
      const edge = edges.find((e) => e.id === id);
      if (edge) onEdgeClick?.(edge);
    },
    [nodes, edges, selectedEdgeIds, onEdgesChange, onNodesChange, onEdgeClick]
  );
  const clearSelection = useCallback(() => {
    const ns = [];
    const es = [];
    for (const n of nodes) if (n.selected) ns.push(change.node.select(n.id, false));
    for (const e of edges) if (e.selected) es.push(change.edge.select(e.id, false));
    if (ns.length) onNodesChange?.(ns);
    if (es.length) onEdgesChange?.(es);
  }, [nodes, edges, onNodesChange, onEdgesChange]);
  const dragRef = useRef4(null);
  const [draggingId, setDraggingId] = useState2(null);
  const beginNodeDrag = useCallback(
    (nodeId, pointerId, clientX, clientY, altKey = false) => {
      if (!nodesDraggable) return;
      const node = nodes.find((n) => n.id === nodeId);
      if (!node) return;
      const kids = descendantsOf(nodeId, nodes).map((d) => ({
        id: d.id,
        startPosition: d.position
      }));
      dragRef.current = {
        pointerId,
        nodeId,
        startClientX: clientX,
        startClientY: clientY,
        startPosition: node.position,
        descendants: kids,
        altDetach: altKey && !!node.parentId,
        rafScheduled: false,
        nextDelta: null
      };
      setDraggingId(nodeId);
      selectNode(nodeId, false);
    },
    [nodes, nodesDraggable, selectNode]
  );
  const [conn, setConn] = useState2(null);
  const connRef = useRef4(null);
  useEffect4(() => {
    connRef.current = conn;
  }, [conn]);
  const beginConnection = useCallback(
    (nodeId, handleId, handleType, pointerId, clientX, clientY) => {
      if (!nodesConnectable) return;
      const node = nodes.find((n) => n.id === nodeId);
      if (!node) return;
      const desc = handleRegistry.resolve(nodeId, handleType, handleId);
      const side = desc?.side ?? (handleType === "source" ? "bottom" : "top");
      const index = desc?.index ?? 0;
      const total = desc?.total ?? 1;
      const ratio = (index + 1) / (total + 1);
      const w = node.width ?? 368;
      const h = effectiveHeight(node);
      const start = (() => {
        switch (side) {
          case "top":
            return { x: node.position.x + w * ratio, y: node.position.y };
          case "bottom":
            return { x: node.position.x + w * ratio, y: node.position.y + h };
          case "left":
            return { x: node.position.x, y: node.position.y + h * ratio };
          case "right":
            return { x: node.position.x + w, y: node.position.y + h * ratio };
        }
      })();
      const from = { nodeId, handleId, handleType };
      const rect = containerRef.current?.getBoundingClientRect();
      const end = rect ? screenToFlow({ x: clientX - rect.left, y: clientY - rect.top }, viewport) : start;
      setConn({ pointerId, from, start, end });
      store.setConnection(from);
      onConnectStart?.({ clientX, clientY, pointerId }, from);
    },
    [nodes, nodesConnectable, handleRegistry, viewport, store, onConnectStart]
  );
  const viewportRef = useRef4(viewport);
  const nodesRef = useRef4(nodes);
  const edgesRef = useRef4(edges);
  const onNodesChangeRefForInstance = useRef4(onNodesChange);
  const onEdgesChangeRefForInstance = useRef4(onEdgesChange);
  const onBeforeDeleteRef = useRef4(onBeforeDelete);
  const snapToGridRef = useRef4(snapToGrid);
  const gridSizeRef = useRef4(gridSize);
  const nodeCollisionGapRef = useRef4(nodeCollisionGap);
  const subflowCollisionGapRef = useRef4(subflowCollisionGap ?? nodeCollisionGap);
  useEffect4(() => {
    edgesRef.current = edges;
  }, [edges]);
  useEffect4(() => {
    onNodesChangeRefForInstance.current = onNodesChange;
  }, [onNodesChange]);
  useEffect4(() => {
    onEdgesChangeRefForInstance.current = onEdgesChange;
  }, [onEdgesChange]);
  useEffect4(() => {
    onBeforeDeleteRef.current = onBeforeDelete;
  }, [onBeforeDelete]);
  useEffect4(() => {
    snapToGridRef.current = snapToGrid;
  }, [snapToGrid]);
  useEffect4(() => {
    gridSizeRef.current = gridSize;
  }, [gridSize]);
  useEffect4(() => {
    nodeCollisionGapRef.current = nodeCollisionGap;
  }, [nodeCollisionGap]);
  useEffect4(() => {
    subflowCollisionGapRef.current = subflowCollisionGap ?? nodeCollisionGap;
  }, [subflowCollisionGap, nodeCollisionGap]);
  const onNodesChangeRef = useRef4(onNodesChange);
  const onConnectRef = useRef4(onConnect);
  const onConnectEndRef = useRef4(onConnectEnd);
  const isValidConnectionRef = useRef4(isValidConnection);
  useEffect4(() => {
    viewportRef.current = viewport;
  }, [viewport]);
  useEffect4(() => {
    nodesRef.current = nodes;
  }, [nodes]);
  useEffect4(() => {
    onNodesChangeRef.current = onNodesChange;
  }, [onNodesChange]);
  useEffect4(() => {
    onConnectRef.current = onConnect;
  }, [onConnect]);
  useEffect4(() => {
    onConnectEndRef.current = onConnectEnd;
  }, [onConnectEnd]);
  useEffect4(() => {
    isValidConnectionRef.current = isValidConnection;
  }, [isValidConnection]);
  useEffect4(() => {
    const onPointerMove = (e) => {
      const vp = viewportRef.current;
      const drag = dragRef.current;
      if (drag && drag.pointerId === e.pointerId) {
        const dx = (e.clientX - drag.startClientX) / vp.zoom;
        const dy = (e.clientY - drag.startClientY) / vp.zoom;
        drag.nextDelta = { dx, dy };
        if (!drag.rafScheduled) {
          drag.rafScheduled = true;
          requestAnimationFrame(() => {
            const d = dragRef.current;
            if (!d) return;
            d.rafScheduled = false;
            const delta = d.nextDelta;
            if (!delta) return;
            const dragNode = nodesRef.current.find((n) => n.id === d.nodeId);
            if (!dragNode) return;
            const proposed = {
              x: d.startPosition.x + delta.dx,
              y: d.startPosition.y + delta.dy
            };
            const clamped = d.altDetach ? proposed : clampToParentExtent(dragNode, proposed, nodesRef.current);
            const isContainer = dragNode.type === "group" || dragNode.type === "forEach";
            const gap = isContainer ? subflowCollisionGapRef.current : nodeCollisionGapRef.current;
            const excludeIds = /* @__PURE__ */ new Set([d.nodeId, ...d.descendants.map((kid) => kid.id)]);
            const finalPos = resolveNodeCollisions(dragNode, clamped, nodesRef.current, {
              gap,
              exclude: excludeIds
            });
            const realDx = finalPos.x - d.startPosition.x;
            const realDy = finalPos.y - d.startPosition.y;
            const changes = [change.node.position(d.nodeId, finalPos, true)];
            for (const kid of d.descendants) {
              changes.push(
                change.node.position(
                  kid.id,
                  { x: kid.startPosition.x + realDx, y: kid.startPosition.y + realDy },
                  true
                )
              );
            }
            onNodesChangeRef.current?.(changes);
          });
        }
      }
      const c = connRef.current;
      if (c && c.pointerId === e.pointerId) {
        const rect = containerRef.current?.getBoundingClientRect();
        if (rect) {
          const end = screenToFlow({ x: e.clientX - rect.left, y: e.clientY - rect.top }, vp);
          setConn({ ...c, end });
        }
      }
    };
    const onPointerUp = (e) => {
      const vp = viewportRef.current;
      const drag = dragRef.current;
      if (drag && drag.pointerId === e.pointerId) {
        const dx = (e.clientX - drag.startClientX) / vp.zoom;
        const dy = (e.clientY - drag.startClientY) / vp.zoom;
        const dragNode = nodesRef.current.find((n) => n.id === drag.nodeId);
        if (dragNode) {
          let proposed = {
            x: drag.startPosition.x + dx,
            y: drag.startPosition.y + dy
          };
          if (snapToGridRef.current) {
            const g = gridSizeRef.current;
            proposed = {
              x: Math.round(proposed.x / g) * g,
              y: Math.round(proposed.y / g) * g
            };
          }
          const clamped = drag.altDetach ? proposed : clampToParentExtent(dragNode, proposed, nodesRef.current);
          const isContainer = dragNode.type === "group" || dragNode.type === "forEach";
          const gap = isContainer ? subflowCollisionGapRef.current : nodeCollisionGapRef.current;
          const excludeIds = /* @__PURE__ */ new Set([
            drag.nodeId,
            ...drag.descendants.map((kid) => kid.id)
          ]);
          const finalPos = resolveNodeCollisions(dragNode, clamped, nodesRef.current, {
            gap,
            exclude: excludeIds
          });
          const realDx = finalPos.x - drag.startPosition.x;
          const realDy = finalPos.y - drag.startPosition.y;
          const changes = [change.node.position(drag.nodeId, finalPos, false)];
          for (const kid of drag.descendants) {
            changes.push(
              change.node.position(
                kid.id,
                { x: kid.startPosition.x + realDx, y: kid.startPosition.y + realDy },
                false
              )
            );
          }
          if (drag.altDetach && dragNode.parentId) {
            const targetGroup = findContainingGroup(
              {
                x: finalPos.x + (dragNode.width ?? 0) / 2,
                y: finalPos.y + (dragNode.height ?? 0) / 2
              },
              nodesRef.current,
              [drag.nodeId, ...drag.descendants.map((d) => d.id)]
            );
            const nextParentId = targetGroup?.id;
            const updated = {
              ...dragNode,
              position: finalPos,
              parentId: nextParentId,
              // Preserve extent only when staying in a group.
              extent: nextParentId ? dragNode.extent : void 0
            };
            changes.push(change.node.replace(drag.nodeId, updated));
          }
          onNodesChangeRef.current?.(changes);
        }
        dragRef.current = null;
        setDraggingId(null);
      }
      const c = connRef.current;
      if (c && c.pointerId === e.pointerId) {
        const target = e.target;
        const handleEl = target?.closest("[data-handle-id]");
        let connection = null;
        let connectedTo;
        if (handleEl) {
          const targetNodeId = handleEl.dataset.handleNodeId;
          const targetHandleId = handleEl.dataset.handleId;
          const targetType = handleEl.dataset.handleType;
          const connectableEnd = handleEl.dataset.handleConnectableEnd === "true";
          if (connectableEnd && (targetNodeId !== c.from.nodeId || targetHandleId !== c.from.handleId) && targetType !== c.from.handleType) {
            const source = c.from.handleType === "source" ? c.from : { nodeId: targetNodeId, handleId: targetHandleId, handleType: "source" };
            const target2 = c.from.handleType === "target" ? c.from : { nodeId: targetNodeId, handleId: targetHandleId, handleType: "target" };
            connection = {
              source: source.nodeId,
              sourceHandle: source.handleId,
              target: target2.nodeId,
              targetHandle: target2.handleId
            };
            connectedTo = {
              nodeId: targetNodeId,
              handleId: targetHandleId,
              handleType: targetType
            };
            const validator = isValidConnectionRef.current;
            if (validator && !validator(connection)) {
              connection = null;
              connectedTo = void 0;
            }
          }
        }
        const rect = containerRef.current?.getBoundingClientRect();
        const flowPos = rect ? screenToFlow({ x: e.clientX - rect.left, y: e.clientY - rect.top }, vp) : { x: 0, y: 0 };
        const endState = {
          cancelled: !connection,
          position: { x: e.clientX, y: e.clientY },
          flowPosition: flowPos,
          from: c.from,
          to: connectedTo
        };
        if (connection) onConnectRef.current?.(connection);
        onConnectEndRef.current?.(e, endState);
        setConn(null);
        store.setConnection(null);
      }
    };
    const onPointerCancel = () => {
      if (dragRef.current) {
        dragRef.current = null;
        setDraggingId(null);
      }
      if (connRef.current) {
        setConn(null);
        store.setConnection(null);
      }
    };
    window.addEventListener("pointermove", onPointerMove);
    window.addEventListener("pointerup", onPointerUp);
    window.addEventListener("pointercancel", onPointerCancel);
    return () => {
      window.removeEventListener("pointermove", onPointerMove);
      window.removeEventListener("pointerup", onPointerUp);
      window.removeEventListener("pointercancel", onPointerCancel);
    };
  }, [store]);
  const panRef = useRef4(null);
  const onCanvasPointerDown = (e) => {
    if (e.button !== 0) return;
    const t = e.target;
    if (t.closest("[data-node-id]") || t.closest("[data-edge-id]") || t.closest("[data-handle-id]")) {
      return;
    }
    if (!panOnDrag) {
      panRef.current = {
        pointerId: e.pointerId,
        startClientX: e.clientX,
        startClientY: e.clientY,
        startVp: viewport,
        moved: false
      };
      return;
    }
    panRef.current = {
      pointerId: e.pointerId,
      startClientX: e.clientX,
      startClientY: e.clientY,
      startVp: viewport,
      moved: false
    };
  };
  useEffect4(() => {
    const onMove = (e) => {
      const pan = panRef.current;
      if (!pan || pan.pointerId !== e.pointerId) return;
      const dx = e.clientX - pan.startClientX;
      const dy = e.clientY - pan.startClientY;
      if (Math.abs(dx) > paneClickDistance || Math.abs(dy) > paneClickDistance) {
        if (!pan.moved) setPanGesture(true);
        pan.moved = true;
      }
      if (pan.moved && panOnDrag) {
        setViewport({ ...pan.startVp, x: pan.startVp.x + dx, y: pan.startVp.y + dy });
      }
    };
    const onUp = () => {
      const pan = panRef.current;
      if (!pan) return;
      if (!pan.moved) {
        onPaneClick?.();
        if (paneClickClearsSelection) clearSelection();
      }
      panRef.current = null;
      setPanGesture(false);
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    window.addEventListener("pointercancel", onUp);
    return () => {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
      window.removeEventListener("pointercancel", onUp);
    };
  }, [
    setViewport,
    paneClickDistance,
    panOnDrag,
    onPaneClick,
    clearSelection,
    paneClickClearsSelection
  ]);
  const handleWheel = (e) => {
    if (!zoomOnScroll) return;
    e.preventDefault();
    const rect = containerRef.current?.getBoundingClientRect();
    if (!rect) return;
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    const factor = Math.exp(-e.deltaY * 1e-3);
    const nextZoom = Math.max(minZoom, Math.min(maxZoom, viewport.zoom * factor));
    const k = nextZoom / viewport.zoom;
    setViewport({
      zoom: nextZoom,
      x: px - (px - viewport.x) * k,
      y: py - (py - viewport.y) * k
    });
  };
  const dispatch = useCallback(
    (a) => {
      if (a.type === "connection/start") {
        beginConnection(a.nodeId, a.handleId, a.handleType, a.pointerId, a.clientX, a.clientY);
      }
    },
    [beginConnection]
  );
  const reportDimensions = useCallback(
    (nodeId, width2, height2) => {
      const node = nodes.find((n) => n.id === nodeId);
      if (!node) return;
      if (node.width === width2 && node.height === height2) return;
      onNodesChange?.([change.node.dimensions(nodeId, { width: width2, height: height2 })]);
    },
    [nodes, onNodesChange]
  );
  const toggleNodeCollapseImpl = useCallback(
    (nodeId) => {
      const node = nodes.find((n) => n.id === nodeId);
      if (!node) return;
      const prevData = node.data ?? {};
      const nextNode = {
        ...node,
        data: { ...prevData, collapsed: !prevData.collapsed }
      };
      onNodesChange?.([change.node.replace(nodeId, nextNode)]);
    },
    [nodes, onNodesChange]
  );
  const deleteNodeImpl = useCallback(
    (nodeId) => {
      const incidentEdgeIds = edgesRef.current.filter((e) => e.source === nodeId || e.target === nodeId).map((e) => e.id);
      if (incidentEdgeIds.length > 0) {
        onEdgesChange?.(incidentEdgeIds.map((id) => change.edge.remove(id)));
      }
      onNodesChange?.([change.node.remove(nodeId)]);
    },
    [onNodesChange, onEdgesChange]
  );
  const instance = useMemo2(
    () => ({
      // viewport
      getViewport: () => viewportRef.current,
      setViewport: (vp) => setViewport(vp),
      setCenter: (x, y, opts) => {
        const z = opts?.zoom ?? viewportRef.current.zoom;
        const rect = containerRef.current?.getBoundingClientRect();
        if (!rect) return;
        setViewport({
          zoom: z,
          x: rect.width / 2 - x * z,
          y: rect.height / 2 - y * z
        });
      },
      fitView: async (opts) => {
        const rect = containerRef.current?.getBoundingClientRect();
        if (!rect) return false;
        const padding = opts?.padding ?? 80;
        const targetNodes = opts?.nodes ? nodesRef.current.filter((n) => opts.nodes.some((x) => x.id === n.id)) : nodesRef.current.filter((n) => !n.hidden);
        if (targetNodes.length === 0) return false;
        let minX = Number.POSITIVE_INFINITY;
        let minY = Number.POSITIVE_INFINITY;
        let maxX = Number.NEGATIVE_INFINITY;
        let maxY = Number.NEGATIVE_INFINITY;
        for (const n of targetNodes) {
          const w = n.width ?? DEFAULT_NODE_WIDTH;
          const h = effectiveHeight(n);
          if (n.position.x < minX) minX = n.position.x;
          if (n.position.y < minY) minY = n.position.y;
          if (n.position.x + w > maxX) maxX = n.position.x + w;
          if (n.position.y + h > maxY) maxY = n.position.y + h;
        }
        const cw = maxX - minX;
        const ch = maxY - minY;
        const zx = (rect.width - padding * 2) / cw;
        const zy = (rect.height - padding * 2) / ch;
        const zoom = Math.max(
          opts?.minZoom ?? 0.25,
          Math.min(opts?.maxZoom ?? 1.5, Math.min(zx, zy))
        );
        setViewport({
          zoom,
          x: rect.width / 2 - (minX + maxX) / 2 * zoom,
          y: rect.height / 2 - (minY + maxY) / 2 * zoom
        });
        return true;
      },
      zoomIn: (opts) => {
        const step = opts?.step ?? 0.2;
        const next = Math.min(maxZoom, viewportRef.current.zoom * (1 + step));
        setViewport({ ...viewportRef.current, zoom: next });
      },
      zoomOut: (opts) => {
        const step = opts?.step ?? 0.2;
        const next = Math.max(minZoom, viewportRef.current.zoom / (1 + step));
        setViewport({ ...viewportRef.current, zoom: next });
      },
      zoomTo: (level) => {
        setViewport({
          ...viewportRef.current,
          zoom: Math.max(minZoom, Math.min(maxZoom, level))
        });
      },
      // transforms
      screenToFlowPosition: (p) => screenToFlow(p, viewportRef.current),
      flowToScreenPosition: (p) => flowToScreen(p, viewportRef.current),
      // reads
      getNodes: () => nodesRef.current.slice(),
      getEdges: () => edgesRef.current.slice(),
      getNode: (id) => nodesRef.current.find((n) => n.id === id),
      getEdge: (id) => edgesRef.current.find((e) => e.id === id),
      getNodesBounds: (subset) => {
        const pool = subset ? nodesRef.current.filter((n) => subset.some((x) => x.id === n.id)) : nodesRef.current.filter((n) => !n.hidden);
        if (pool.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
        let minX = Number.POSITIVE_INFINITY;
        let minY = Number.POSITIVE_INFINITY;
        let maxX = Number.NEGATIVE_INFINITY;
        let maxY = Number.NEGATIVE_INFINITY;
        for (const n of pool) {
          const w = n.width ?? DEFAULT_NODE_WIDTH;
          const h = effectiveHeight(n);
          minX = Math.min(minX, n.position.x);
          minY = Math.min(minY, n.position.y);
          maxX = Math.max(maxX, n.position.x + w);
          maxY = Math.max(maxY, n.position.y + h);
        }
        return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
      },
      getIntersectingNodes: (area, partially = true) => {
        return nodesRef.current.filter((n) => {
          if (n.hidden) return false;
          const w = n.width ?? DEFAULT_NODE_WIDTH;
          const h = effectiveHeight(n);
          if (partially) {
            return n.position.x < area.x + area.width && n.position.x + w > area.x && n.position.y < area.y + area.height && n.position.y + h > area.y;
          }
          return n.position.x >= area.x && n.position.y >= area.y && n.position.x + w <= area.x + area.width && n.position.y + h <= area.y + area.height;
        });
      },
      // mutations
      addNodes: (nodes2) => {
        const arr = Array.isArray(nodes2) ? nodes2 : [nodes2];
        onNodesChangeRefForInstance.current?.(arr.map((item) => change.node.add(item)));
      },
      addEdges: (edges2) => {
        const arr = Array.isArray(edges2) ? edges2 : [edges2];
        onEdgesChangeRefForInstance.current?.(arr.map((item) => change.edge.add(item)));
      },
      updateNodeData: (id, partial) => {
        const cur = nodesRef.current.find((n) => n.id === id);
        if (!cur) return;
        const nextData = { ...cur.data ?? {}, ...partial };
        onNodesChangeRefForInstance.current?.([
          change.node.replace(id, { ...cur, data: nextData })
        ]);
      },
      updateNode: (id, partial) => {
        const cur = nodesRef.current.find((n) => n.id === id);
        if (!cur) return;
        onNodesChangeRefForInstance.current?.([change.node.replace(id, { ...cur, ...partial })]);
      },
      deleteElements: async ({ nodes: ns, edges: es }) => {
        const nodesToDelete = (ns ?? []).map((x) => nodesRef.current.find((n) => n.id === x.id)).filter((n) => !!n);
        const edgesToDelete = (es ?? []).map((x) => edgesRef.current.find((e) => e.id === x.id)).filter((e) => !!e);
        const before = onBeforeDeleteRef.current;
        if (before) {
          const ok = await before({ nodes: nodesToDelete, edges: edgesToDelete });
          if (!ok) return false;
        }
        if (nodesToDelete.length > 0) {
          onNodesChangeRefForInstance.current?.(nodesToDelete.map((n) => change.node.remove(n.id)));
        }
        if (edgesToDelete.length > 0) {
          onEdgesChangeRefForInstance.current?.(edgesToDelete.map((e) => change.edge.remove(e.id)));
        }
        return true;
      }
    }),
    [setViewport, minZoom, maxZoom]
  );
  const initFiredRef = useRef4(false);
  useEffect4(() => {
    if (initFiredRef.current) return;
    initFiredRef.current = true;
    onInit?.(instance);
  }, [instance, onInit]);
  useEffect4(() => {
    const onKey = (e) => {
      if (e.key !== "Backspace" && e.key !== "Delete") return;
      const target = e.target;
      if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
        return;
      }
      if (!containerRef.current?.contains(target) && document.activeElement !== document.body) {
        return;
      }
      const sel = store.getSnapshot();
      if (sel.selectedNodeIds.size === 0 && sel.selectedEdgeIds.size === 0) return;
      e.preventDefault();
      void instance.deleteElements({
        nodes: Array.from(sel.selectedNodeIds, (id) => ({ id })),
        edges: Array.from(sel.selectedEdgeIds, (id) => ({ id }))
      });
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [instance, store]);
  const bridge = useMemo2(
    () => ({
      beginNodeDrag,
      selectNode,
      notifyNodeClick,
      selectEdge,
      reportDimensions,
      deleteNode: deleteNodeImpl,
      toggleNodeCollapse: toggleNodeCollapseImpl
    }),
    [
      beginNodeDrag,
      selectNode,
      notifyNodeClick,
      selectEdge,
      reportDimensions,
      deleteNodeImpl,
      toggleNodeCollapseImpl
    ]
  );
  const [panGesture, setPanGesture] = useState2(false);
  const isEmpty = nodes.length === 0 && edges.length === 0;
  const isConnecting = conn !== null;
  const visibleNodes = useMemo2(() => nodes.filter((n) => !n.hidden), [nodes]);
  const visibleEdges = useMemo2(() => {
    if (visibleNodes.length === nodes.length) return edges;
    const visibleIds = new Set(visibleNodes.map((n) => n.id));
    return edges.filter((e) => visibleIds.has(e.source) && visibleIds.has(e.target));
  }, [edges, nodes, visibleNodes]);
  const orderedNodes = useMemo2(() => {
    const isContainer = (n) => n.type === "group" || n.type === "forEach";
    const depth = (n) => {
      let d = 0;
      let cursor = n.parentId;
      const seen = /* @__PURE__ */ new Set();
      while (cursor) {
        if (seen.has(cursor)) return Number.POSITIVE_INFINITY;
        seen.add(cursor);
        d++;
        cursor = visibleNodes.find((x) => x.id === cursor)?.parentId;
      }
      return d;
    };
    const containers = visibleNodes.filter(isContainer);
    const others = visibleNodes.filter((n) => !isContainer(n));
    containers.sort((a, b) => depth(a) - depth(b));
    return [...containers, ...others];
  }, [visibleNodes]);
  return /* @__PURE__ */ jsx6(FlowStoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx6(FlowInstanceContext.Provider, { value: instance, children: /* @__PURE__ */ jsx6(HandleRegistryContext.Provider, { value: handleRegistry, children: /* @__PURE__ */ jsx6(FlowDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx6(FlowNodeBridgeContext.Provider, { value: bridge, children: /* @__PURE__ */ jsxs5(
    "div",
    {
      ref: containerRef,
      className: cn(
        "ods-flow-canvas-v2",
        panGesture && "ods-flow-canvas-v2--panning",
        isConnecting && "ods-flow-canvas-v2--connecting",
        draggingId && "ods-flow-canvas-v2--dragging",
        className
      ),
      style: {
        position: "relative",
        width,
        height,
        overflow: "hidden",
        userSelect: "none",
        touchAction: "none",
        // Cursor: state-driven so it flips instantly with the
        // gesture. CSS `:active` would lag behind pointer-capture.
        cursor: panGesture ? "grabbing" : isConnecting ? "crosshair" : panOnDrag ? "grab" : "default",
        ...style
      },
      onPointerDown: onCanvasPointerDown,
      onWheel: handleWheel,
      onContextMenu: (e) => {
        const t = e.target;
        const nodeEl = t.closest("[data-node-id]");
        if (nodeEl && onNodeContextMenu) {
          const id = nodeEl.dataset.nodeId;
          const node = nodes.find((n) => n.id === id);
          if (node) onNodeContextMenu(e, node);
          return;
        }
        const edgeEl = t.closest("[data-edge-id]");
        if (edgeEl && onEdgeContextMenu) {
          const id = edgeEl.dataset.edgeId;
          const edge = edges.find((x) => x.id === id);
          if (edge) onEdgeContextMenu(e, edge);
          return;
        }
        onPaneContextMenu?.(e);
      },
      "data-empty": isEmpty ? "true" : void 0,
      children: [
        background !== "none" && /* @__PURE__ */ jsx6(
          "div",
          {
            className: cn(
              "ods-flow-canvas-v2__grid",
              `ods-flow-canvas-v2__grid--${background}`
            ),
            style: {
              // Custom property drives the four variants' SCSS.
              "--ods-flow-grid-size": `${gridSize * viewport.zoom}px`,
              backgroundPosition: `${viewport.x}px ${viewport.y}px`
            }
          }
        ),
        /* @__PURE__ */ jsxs5(
          "div",
          {
            className: "ods-flow-canvas-v2__viewport",
            style: {
              position: "absolute",
              inset: 0,
              transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
              transformOrigin: "0 0"
            },
            children: [
              /* @__PURE__ */ jsx6(
                EdgesLayer,
                {
                  edges: visibleEdges,
                  nodes: visibleNodes,
                  onSelect: (id) => bridge.selectEdge(id, false),
                  onDelete: (id) => onEdgesChangeRef(id, onEdgesChange),
                  onLabelChange: onEdgeLabelChange,
                  ghost: conn ? { start: conn.start, end: conn.end } : null,
                  handleVersion
                }
              ),
              orderedNodes.map((node) => {
                const Kind = kinds[node.type] ?? kinds.action;
                if (!Kind) return null;
                return /* @__PURE__ */ jsx6(
                  FlowNode,
                  {
                    node,
                    selected: selectedNodeIds.has(node.id),
                    dragging: draggingId === node.id,
                    isConnecting,
                    Kind
                  },
                  node.id
                );
              })
            ]
          }
        ),
        isEmpty && emptyState && /* @__PURE__ */ jsx6("div", { className: "ods-flow-canvas-v2__empty", children: emptyState }),
        children
      ]
    }
  ) }) }) }) }) });
}
function onEdgesChangeRef(id, cb) {
  cb?.([change.edge.remove(id)]);
}
var EdgesLayer = memo2(function EdgesLayer2({
  edges,
  nodes,
  onSelect,
  onDelete,
  onLabelChange,
  ghost,
  handleVersion: _handleVersion
}) {
  return /* @__PURE__ */ jsxs5(
    "svg",
    {
      className: "ods-flow-canvas-v2__edges",
      style: { position: "absolute", inset: 0, overflow: "visible", pointerEvents: "none" },
      width: "100%",
      height: "100%",
      children: [
        /* @__PURE__ */ jsx6("g", { style: { pointerEvents: "auto" }, children: edges.map((edge) => /* @__PURE__ */ jsx6(
          FlowEdge,
          {
            edge,
            nodes,
            onSelect,
            onDelete,
            onLabelChange,
            handleVersion: _handleVersion
          },
          edge.id
        )) }),
        ghost && /* @__PURE__ */ jsx6(
          "path",
          {
            d: `M ${ghost.start.x} ${ghost.start.y} L ${ghost.end.x} ${ghost.end.y}`,
            stroke: "var(--ods-accent, #4f46e5)",
            strokeWidth: 1.5,
            strokeDasharray: "4 4",
            fill: "none"
          }
        )
      ]
    }
  );
});

export {
  buildEdgePath,
  bezierPath,
  stepPath,
  smoothStepPath,
  straightPath,
  useFlow,
  applyNodeChanges,
  applyEdgeChanges,
  change,
  useFlowStore,
  DEFAULT_NODE_WIDTH,
  DEFAULT_NODE_HEIGHT,
  handleCentre,
  bezierPath2,
  screenToFlow,
  flowToScreen,
  descendantsOf,
  findAncestor,
  clampToParentExtent,
  findContainingGroup,
  FlowEdge,
  useFlowNodeContext,
  FlowNode,
  buildNodeKindRegistry,
  Handle,
  ActionNode,
  TriggerNode,
  ConditionNode,
  GroupNode,
  ForEachNode,
  OutputNode,
  ErrorNode,
  WaitNode,
  ParallelNode,
  StickyNode,
  WebhookNode,
  HttpRequestNode,
  DEFAULT_NODE_KINDS,
  FlowCanvas
};
//# sourceMappingURL=chunk-KY4JHPIE.js.map