{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "workflow-timer",
  "title": "Workflow Timer",
  "description": "A tabular timer for how long a step or run has been going.",
  "dependencies": [
    "@xyflow/react",
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/notra/workflow/components/workflow-timer.tsx",
      "content": "import { cn } from \"cn\";\n\nimport { formatDuration } from \"../lib/workflow\";\nimport type { WorkflowTimerProps } from \"../types/workflow\";\n\nexport const WorkflowTimer = ({\n  className,\n  ms,\n  running,\n  ...props\n}: WorkflowTimerProps) => (\n  <span\n    className={cn(\n      \"text-muted-foreground inline-flex items-center gap-1 font-mono text-[0.6875rem] leading-4 tabular-nums\",\n      running && \"text-foreground\",\n      className\n    )}\n    data-running={running || undefined}\n    data-slot=\"workflow-timer\"\n    {...props}\n  >\n    {running && (\n      <span\n        aria-hidden=\"true\"\n        className=\"bg-primary size-1.5 rounded-full motion-safe:animate-pulse\"\n      />\n    )}\n    <time dateTime={`PT${(ms / 1000).toFixed(1)}S`}>{formatDuration(ms)}</time>\n  </span>\n);\n",
      "type": "registry:component",
      "target": "components/workflow/components/workflow-timer.tsx"
    },
    {
      "path": "registry/notra/workflow/constants/workflow-ui.ts",
      "content": "import type {\n  WorkflowBannerConfig,\n  WorkflowEdgeStatus,\n  WorkflowRunPhase,\n} from \"../types/workflow\";\n\nexport const WORKFLOW_NODE_WIDTH = 256;\n\nexport const WORKFLOW_NODE_DROP_OFFSET_Y = 24;\n\nexport const WORKFLOW_COLUMN = 320;\n\nexport const WORKFLOW_FIT_PADDING = 0.12;\n\nexport const WORKFLOW_FIT_MIN_ZOOM = 0.6;\n\nexport const WORKFLOW_FIT_MAX_ZOOM = 1;\n\nexport const WORKFLOW_FIT_EDGE_PX = 32;\n\nexport const WORKFLOW_FIT_VIEW_OPTIONS = {\n  maxZoom: WORKFLOW_FIT_MAX_ZOOM,\n  padding: WORKFLOW_FIT_PADDING,\n};\n\nexport const WORKFLOW_ZOOM_DURATION_MS = 200;\n\nexport const WORKFLOW_FIT_DURATION_MS = 300;\n\nexport const WORKFLOW_MINIMAP_STYLE = { height: 96, width: 144 };\n\nexport const WORKFLOW_PATTERN_STRENGTH = {\n  cross: 22,\n  dots: 24,\n  gridMajor: 12,\n  gridMinor: 5,\n  lines: 7,\n};\n\nexport const WORKFLOW_PORT_ROW_CLASS =\n  \"text-muted-foreground border-table-card-border relative flex h-7.5 min-w-0 items-center border-t px-3 text-[0.6875rem] font-medium\";\n\nexport const WORKFLOW_CHIP_CLASS =\n  \"bg-foreground/[0.05] text-muted-foreground inline-flex h-5 min-w-0 items-center gap-1 rounded-full px-2 text-[0.6875rem] leading-none tabular-nums [&_svg]:size-3 [&_svg]:shrink-0\";\n\nexport const WORKFLOW_BANNERS: Record<\n  \"error\" | \"waiting\",\n  WorkflowBannerConfig\n> = {\n  error: {\n    className: \"text-red-600 dark:text-red-400\",\n    label: \"Failed\",\n    message: \"Check the run log\",\n  },\n  waiting: {\n    className: \"text-amber-600 dark:text-amber-400\",\n    label: \"Waiting\",\n    message: \"Needs approval\",\n  },\n};\n\nexport const WORKFLOW_PILL_CLASS =\n  \"bg-table-shell border-table-shell-border text-muted-foreground inline-flex h-9 w-7 shrink-0 flex-col overflow-hidden rounded-lg border [&_svg]:size-3\";\n\nexport const WORKFLOW_PILL_TOP_CLASS =\n  \"flex flex-1 items-center justify-center\";\n\nexport const WORKFLOW_PILL_BOTTOM_CLASS =\n  \"border-table-shell-border flex flex-1 items-center justify-center border-t\";\n\nexport const WORKFLOW_LED_CLASS =\n  \"ring-background/60 size-1.5 rounded-full ring-2 transition-colors duration-300\";\n\nexport const WORKFLOW_EDGE_COLOR: Record<WorkflowEdgeStatus, string> = {\n  active: \"text-primary\",\n  done: \"text-primary/70\",\n  idle: \"text-muted-foreground/70\",\n  skipped: \"text-muted-foreground/25\",\n};\n\nexport const WORKFLOW_EDGE_DASH = \"0 10\";\n\nexport const WORKFLOW_EDGE_STEP_OPTIONS = { borderRadius: 24, offset: 24 };\n\nexport const WORKFLOW_EDGE_RADIUS = 24;\n\nexport const WORKFLOW_EDGE_LANE_GAP = 16;\n\nexport const WORKFLOW_RUN_LABELS: Record<WorkflowRunPhase, string> = {\n  done: \"Run again\",\n  idle: \"Run workflow\",\n  running: \"Stop\",\n};\n\nexport const WORKFLOW_RUN_RING_RADIUS = 10;\n\nexport const WORKFLOW_RUN_RING_LENGTH = 2 * Math.PI * WORKFLOW_RUN_RING_RADIUS;\n\nexport const WORKFLOW_SECTION_TITLE_CLASS =\n  \"text-muted-foreground text-[0.6875rem] font-medium tracking-wide uppercase\";\n\nexport const WORKFLOW_EDGE_REVERSE_STYLE = { animationDirection: \"reverse\" };\n\nexport const WORKFLOW_SKIPPED_DIM_CLASS =\n  \"transition-opacity duration-300 group-data-[status=skipped]/node:opacity-45\";\n\nexport const WORKFLOW_BRANCH_RAIL_CLASS =\n  \"border-table-card-border bg-table-card/60 divide-table-card-border mt-0.5 divide-y rounded-[14px] border\";\n\nexport const WORKFLOW_BRANCH_ROW_CLASS =\n  \"relative flex h-10 min-w-0 items-center gap-2.5 px-3\";\n\nexport const WORKFLOW_BRANCH_LED_CLASS =\n  \"size-2 shrink-0 rounded-full ring-2 transition-[background-color,box-shadow,opacity] duration-300\";\n\nexport const WORKFLOW_RULE_TOKEN_CLASS =\n  \"bg-foreground/[0.05] text-foreground/80 inline-flex h-5 max-w-full min-w-0 items-center truncate rounded-md px-1.5 text-[0.6875rem] whitespace-nowrap\";\n",
      "type": "registry:lib",
      "target": "components/workflow/constants/workflow-ui.ts"
    },
    {
      "path": "registry/notra/workflow/constants/workflow.ts",
      "content": "import {\n  FlagIcon,\n  GitForkIcon,\n  HourglassIcon,\n  MailIcon,\n  MergeIcon,\n  PlugIcon,\n  RepeatIcon,\n  SparklesIcon,\n  SplitIcon,\n  SquareFunctionIcon,\n  UserCheckIcon,\n  ZapIcon,\n} from \"lucide-react\";\n\nimport type {\n  WorkflowKindConfig,\n  WorkflowModel,\n  WorkflowNodeKind,\n  WorkflowNodeStatus,\n  WorkflowPort,\n  WorkflowStatusConfig,\n} from \"../types/workflow\";\n\nexport const WORKFLOW_DEFAULT_DURATION_MS = 900;\n\nexport const WORKFLOW_EDGE_TRAVEL_MS = 450;\n\nexport const WORKFLOW_TICK_MS = 50;\n\nexport const WORKFLOW_BLOCK_MIME = \"application/x-workflow-block\";\n\nexport const WORKFLOW_IN_PORT: WorkflowPort = { id: \"in\" };\n\nexport const WORKFLOW_OUT_PORT: WorkflowPort = { id: \"out\" };\n\nexport const WORKFLOW_KINDS: Record<WorkflowNodeKind, WorkflowKindConfig> = {\n  ai: {\n    icon: SparklesIcon,\n    label: \"AI step\",\n    tileClassName:\n      \"bg-violet-500/10 text-violet-600 ring-violet-500/20 dark:text-violet-300\",\n  },\n  approval: {\n    icon: UserCheckIcon,\n    label: \"Human approval\",\n    tileClassName:\n      \"bg-pink-500/10 text-pink-600 ring-pink-500/20 dark:text-pink-300\",\n  },\n  condition: {\n    icon: SplitIcon,\n    label: \"If / else\",\n    tileClassName:\n      \"bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-300\",\n  },\n  delay: {\n    icon: HourglassIcon,\n    label: \"Wait\",\n    tileClassName:\n      \"bg-zinc-500/10 text-zinc-600 ring-zinc-500/20 dark:text-zinc-300\",\n  },\n  email: {\n    icon: MailIcon,\n    label: \"Email\",\n    tileClassName:\n      \"bg-emerald-500/10 text-emerald-600 ring-emerald-500/20 dark:text-emerald-300\",\n  },\n  integration: {\n    icon: PlugIcon,\n    label: \"Integration\",\n    tileClassName:\n      \"bg-sky-500/10 text-sky-600 ring-sky-500/20 dark:text-sky-300\",\n  },\n  loop: {\n    icon: RepeatIcon,\n    label: \"For each\",\n    tileClassName:\n      \"bg-indigo-500/10 text-indigo-600 ring-indigo-500/20 dark:text-indigo-300\",\n  },\n  merge: {\n    icon: MergeIcon,\n    label: \"Merge\",\n    tileClassName:\n      \"bg-teal-500/10 text-teal-600 ring-teal-500/20 dark:text-teal-300\",\n  },\n  output: {\n    icon: FlagIcon,\n    label: \"Output\",\n    tileClassName:\n      \"bg-zinc-500/10 text-zinc-600 ring-zinc-500/20 dark:text-zinc-300\",\n  },\n  switch: {\n    icon: GitForkIcon,\n    label: \"Decision\",\n    tileClassName:\n      \"bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-300\",\n  },\n  task: {\n    icon: SquareFunctionIcon,\n    label: \"Task\",\n    tileClassName:\n      \"bg-blue-500/10 text-blue-600 ring-blue-500/20 dark:text-blue-300\",\n  },\n  trigger: {\n    icon: ZapIcon,\n    label: \"Trigger\",\n    tileClassName:\n      \"bg-amber-500/10 text-amber-600 ring-amber-500/20 dark:text-amber-300\",\n  },\n};\n\nexport const WORKFLOW_NON_DETERMINISTIC_KINDS: WorkflowNodeKind[] = [\"ai\"];\n\nexport const WORKFLOW_STATUSES: Record<\n  WorkflowNodeStatus,\n  WorkflowStatusConfig\n> = {\n  error: {\n    className: \"text-red-600 dark:text-red-400\",\n    label: \"Failed\",\n    ledClassName: \"bg-red-500 shadow-[0_0_6px_var(--color-red-500)]\",\n  },\n  idle: {\n    className: \"text-muted-foreground\",\n    label: \"Idle\",\n    ledClassName: \"bg-muted-foreground/35\",\n  },\n  queued: {\n    className: \"text-muted-foreground\",\n    label: \"Queued\",\n    ledClassName: \"bg-muted-foreground/35\",\n  },\n  running: {\n    className: \"text-primary\",\n    label: \"Running\",\n    ledClassName:\n      \"bg-primary shadow-[0_0_6px_var(--color-primary,var(--primary))] motion-safe:animate-pulse\",\n  },\n  skipped: {\n    className: \"text-muted-foreground\",\n    label: \"Skipped\",\n    ledClassName: \"bg-muted-foreground/20\",\n  },\n  success: {\n    className: \"text-emerald-600 dark:text-emerald-400\",\n    label: \"Completed\",\n    ledClassName: \"bg-emerald-500 shadow-[0_0_6px_var(--color-emerald-500)]\",\n  },\n  waiting: {\n    className: \"text-amber-600 dark:text-amber-400\",\n    label: \"Waiting for approval\",\n    ledClassName:\n      \"bg-amber-500 shadow-[0_0_6px_var(--color-amber-500)] motion-safe:animate-pulse\",\n  },\n};\n\nexport const WORKFLOW_NODE_RING: Record<WorkflowNodeStatus, string> = {\n  error: \"border-red-500/60\",\n  idle: \"\",\n  queued: \"\",\n  running:\n    \"border-primary shadow-[0_0_0_0.3rem_color-mix(in_oklab,var(--color-primary,var(--primary))_14%,transparent)]\",\n  skipped: \"\",\n  success: \"border-emerald-500/50\",\n  waiting:\n    \"border-amber-500 shadow-[0_0_0_0.3rem_color-mix(in_oklab,var(--color-amber-500)_14%,transparent)]\",\n};\n\nexport const WORKFLOW_MODELS: WorkflowModel[] = [\n  {\n    id: \"claude-opus-5-5\",\n    label: \"Claude Opus 5.5\",\n    logo: \"claude\",\n    provider: \"Anthropic\",\n  },\n  {\n    id: \"claude-sonnet-5-5\",\n    label: \"Claude Sonnet 5.5\",\n    logo: \"claude\",\n    provider: \"Anthropic\",\n  },\n  {\n    id: \"claude-haiku-5-5\",\n    label: \"Claude Haiku 5.5\",\n    logo: \"claude\",\n    provider: \"Anthropic\",\n  },\n  { id: \"gpt-5\", label: \"GPT-5\", logo: \"openai\", provider: \"OpenAI\" },\n  { id: \"gpt-5-mini\", label: \"GPT-5 mini\", logo: \"openai\", provider: \"OpenAI\" },\n  {\n    id: \"gemini-2-5-pro\",\n    label: \"Gemini 2.5 Pro\",\n    logo: \"gemini\",\n    provider: \"Google\",\n  },\n  {\n    id: \"sonar-pro\",\n    label: \"Sonar Pro\",\n    logo: \"perplexity\",\n    provider: \"Perplexity\",\n  },\n  { id: \"grok-4\", label: \"Grok 4\", logo: \"grok\", provider: \"xAI\" },\n  {\n    id: \"mistral-large\",\n    label: \"Mistral Large\",\n    logo: \"mistral\",\n    provider: \"Mistral\",\n  },\n  {\n    id: \"deepseek-v3\",\n    label: \"DeepSeek V3\",\n    logo: \"deepseek\",\n    provider: \"DeepSeek\",\n  },\n];\n",
      "type": "registry:lib",
      "target": "components/workflow/constants/workflow.ts"
    },
    {
      "path": "registry/notra/workflow/lib/workflow.ts",
      "content": "import { getSmoothStepPath } from \"@xyflow/react\";\nimport type { ReactFlowState } from \"@xyflow/react\";\n\nimport {\n  WORKFLOW_DEFAULT_DURATION_MS,\n  WORKFLOW_EDGE_TRAVEL_MS,\n  WORKFLOW_IN_PORT,\n  WORKFLOW_MODELS,\n  WORKFLOW_NON_DETERMINISTIC_KINDS,\n  WORKFLOW_OUT_PORT,\n} from \"../constants/workflow\";\nimport {\n  WORKFLOW_EDGE_LANE_GAP,\n  WORKFLOW_EDGE_RADIUS,\n  WORKFLOW_EDGE_STEP_OPTIONS,\n} from \"../constants/workflow-ui\";\nimport type {\n  WorkflowAnchor,\n  WorkflowBlock,\n  WorkflowEdgeData,\n  WorkflowEdgeLane,\n  WorkflowEdgeStatus,\n  WorkflowLogEntry,\n  WorkflowNodeData,\n  WorkflowNodeState,\n  WorkflowPoint,\n  WorkflowPort,\n  WorkflowRunPhase,\n  WorkflowSchedule,\n} from \"../types/workflow\";\n\nexport const getInputPorts = (node: WorkflowNodeData): WorkflowPort[] =>\n  node.inputs ?? (node.kind === \"trigger\" ? [] : [WORKFLOW_IN_PORT]);\n\nexport const getOutputPorts = (node: WorkflowNodeData): WorkflowPort[] =>\n  node.outputs ?? (node.kind === \"output\" ? [] : [WORKFLOW_OUT_PORT]);\n\nexport const isDeterministic = (node: Pick<WorkflowNodeData, \"kind\">) =>\n  !WORKFLOW_NON_DETERMINISTIC_KINDS.includes(node.kind);\n\nexport const findModel = (id?: string) =>\n  WORKFLOW_MODELS.find((model) => model.id === id);\n\nconst byPosition = (a: WorkflowNodeData, b: WorkflowNodeData) =>\n  a.position.x - b.position.x || a.position.y - b.position.y;\n\nexport const getRunOrder = (\n  nodes: WorkflowNodeData[],\n  edges: WorkflowEdgeData[]\n) => {\n  const indegree = new Map(nodes.map((node) => [node.id, 0]));\n  for (const edge of edges) {\n    if (indegree.has(edge.target) && indegree.has(edge.source)) {\n      indegree.set(edge.target, (indegree.get(edge.target) ?? 0) + 1);\n    }\n  }\n\n  const byId = new Map(nodes.map((node) => [node.id, node]));\n  const ready = nodes.filter((node) => indegree.get(node.id) === 0);\n  const order: string[] = [];\n  while (ready.length > 0) {\n    ready.sort(byPosition);\n    const node = ready.shift();\n    if (!node) {\n      break;\n    }\n    order.push(node.id);\n    for (const edge of edges) {\n      if (edge.source !== node.id) {\n        continue;\n      }\n      const left = (indegree.get(edge.target) ?? 0) - 1;\n      indegree.set(edge.target, left);\n      const target = byId.get(edge.target);\n      if (left === 0 && target) {\n        ready.push(target);\n      }\n    }\n  }\n\n  const ordered = new Set(order);\n  const rest = nodes.filter((node) => !ordered.has(node.id)).sort(byPosition);\n  return [...order, ...rest.map((node) => node.id)];\n};\n\nexport const scheduleRun = (\n  nodes: WorkflowNodeData[],\n  edges: WorkflowEdgeData[]\n): WorkflowSchedule => {\n  const order = getRunOrder(nodes, edges);\n  const schedule: WorkflowSchedule = {\n    edges: {},\n    nodes: {},\n    order,\n    totalMs: 0,\n  };\n\n  const byId = new Map(nodes.map((node) => [node.id, node]));\n  for (const id of order) {\n    const node = byId.get(id);\n    if (!node) {\n      continue;\n    }\n    const incoming = edges\n      .filter((edge) => edge.target === id)\n      .map((edge) => schedule.edges[edge.id])\n      .filter((run) => run !== undefined);\n    const taken = incoming.filter((run) => run.taken);\n    const settled = Math.max(0, ...incoming.map((run) => run.end));\n\n    if (incoming.length > 0 && taken.length === 0) {\n      schedule.nodes[id] = { end: settled, start: settled, status: \"skipped\" };\n    } else {\n      const start = Math.max(0, ...taken.map((run) => run.end));\n      schedule.nodes[id] = {\n        end: start + (node.durationMs ?? WORKFLOW_DEFAULT_DURATION_MS),\n        start,\n        status: node.fail ? \"error\" : \"success\",\n      };\n    }\n\n    const run = schedule.nodes[id];\n    for (const edge of edges.filter((item) => item.source === id)) {\n      const port = edge.sourcePort ?? WORKFLOW_OUT_PORT.id;\n      const isTaken =\n        run.status === \"success\" &&\n        (node.branch === undefined || node.branch === port);\n      schedule.edges[edge.id] = {\n        end: run.end + (isTaken ? WORKFLOW_EDGE_TRAVEL_MS : 0),\n        start: run.end,\n        taken: isTaken,\n      };\n    }\n    schedule.totalMs = Math.max(schedule.totalMs, run.end);\n  }\n\n  return schedule;\n};\n\nexport const getNodeState = (\n  node: WorkflowNodeData | undefined,\n  schedule: WorkflowSchedule,\n  phase: WorkflowRunPhase,\n  elapsedMs: number\n): WorkflowNodeState => {\n  const run = node ? schedule.nodes[node.id] : undefined;\n  if (!(node && run) || phase === \"idle\") {\n    return { elapsedMs: 0, status: \"idle\" };\n  }\n  if (elapsedMs < run.start) {\n    return { elapsedMs: 0, status: \"queued\" };\n  }\n  if (run.status === \"skipped\") {\n    return { elapsedMs: 0, status: \"skipped\" };\n  }\n  if (elapsedMs < run.end) {\n    return {\n      elapsedMs: elapsedMs - run.start,\n      status: node.kind === \"approval\" ? \"waiting\" : \"running\",\n    };\n  }\n  return { elapsedMs: run.end - run.start, status: run.status };\n};\n\nexport const getEdgeState = (\n  id: string,\n  schedule: WorkflowSchedule,\n  phase: WorkflowRunPhase,\n  elapsedMs: number\n): WorkflowEdgeStatus => {\n  const run = schedule.edges[id];\n  if (!run || phase === \"idle\" || elapsedMs < run.start) {\n    return \"idle\";\n  }\n  if (!run.taken) {\n    return \"skipped\";\n  }\n  return elapsedMs < run.end ? \"active\" : \"done\";\n};\n\nexport const formatDuration = (ms: number) => {\n  if (ms < 60_000) {\n    return `${(ms / 1000).toFixed(1)}s`;\n  }\n  if (ms < 3_600_000) {\n    const minutes = Math.floor(ms / 60_000);\n    const seconds = Math.floor((ms % 60_000) / 1000);\n    return `${minutes}m ${String(seconds).padStart(2, \"0\")}s`;\n  }\n  const hours = Math.floor(ms / 3_600_000);\n  const minutes = Math.floor((ms % 3_600_000) / 60_000);\n  return `${hours}h ${String(minutes).padStart(2, \"0\")}m`;\n};\n\nexport const formatRate = (rate: number) => `${Math.round(rate * 100)}%`;\n\nexport const formatClock = (ms: number) => {\n  const minutes = Math.floor(ms / 60_000);\n  const seconds = Math.floor((ms % 60_000) / 1000);\n  const tenths = Math.floor((ms % 1000) / 100);\n  return `${String(minutes).padStart(2, \"0\")}:${String(seconds).padStart(2, \"0\")}.${tenths}`;\n};\n\nconst branchLabel = (node: WorkflowNodeData) =>\n  getOutputPorts(node).find((port) => port.id === node.branch)?.label ??\n  node.branch;\n\nexport const getRunLog = (\n  nodes: WorkflowNodeData[],\n  schedule: WorkflowSchedule,\n  phase: WorkflowRunPhase,\n  elapsedMs: number\n): WorkflowLogEntry[] => {\n  if (phase === \"idle\") {\n    return [];\n  }\n  const entries: WorkflowLogEntry[] = [];\n  const byId = new Map(nodes.map((node) => [node.id, node]));\n  for (const id of schedule.order) {\n    const node = byId.get(id);\n    const run = schedule.nodes[id];\n    if (!(node && run) || elapsedMs < run.start) {\n      continue;\n    }\n    const base = { nodeId: id, title: node.title };\n    if (run.status === \"skipped\") {\n      entries.push({\n        ...base,\n        at: run.start,\n        id: `${id}:skip`,\n        level: \"skip\",\n        message: \"Skipped, no earlier step led here\",\n      });\n      continue;\n    }\n    entries.push({\n      ...base,\n      at: run.start,\n      id: `${id}:start`,\n      level: \"info\",\n      message: node.kind === \"approval\" ? \"Waiting for approval\" : \"Started\",\n    });\n    if (elapsedMs < run.end) {\n      continue;\n    }\n    const took = run.end - run.start;\n    let message = `Completed in ${formatDuration(took)}`;\n    if (run.status === \"error\") {\n      message = `Failed after ${formatDuration(took)}`;\n    } else if (node.branch !== undefined) {\n      message = `Took the \"${branchLabel(node)}\" branch`;\n    }\n    entries.push({\n      ...base,\n      at: run.end,\n      id: `${id}:end`,\n      level: run.status === \"error\" ? \"error\" : \"success\",\n      message,\n    });\n  }\n  return entries.sort((a, b) => a.at - b.at);\n};\n\nconst getEdgeMidX = (\n  from: WorkflowPoint,\n  to: WorkflowPoint,\n  radius: number,\n  offset: number\n) => {\n  const room = Math.max(0, Math.abs(to.x - from.x) / 2 - radius * 2);\n  return (from.x + to.x) / 2 + Math.max(-room, Math.min(room, offset));\n};\n\nexport const getEdgePath = (\n  from: WorkflowPoint,\n  to: WorkflowPoint,\n  radius = WORKFLOW_EDGE_RADIUS,\n  offset = 0\n) => {\n  const dx = to.x - from.x;\n  const dy = to.y - from.y;\n  const midX = getEdgeMidX(from, to, radius, offset);\n  const r = Math.min(\n    radius,\n    Math.abs(dy) / 2,\n    Math.abs(midX - from.x),\n    Math.abs(to.x - midX)\n  );\n  if (r < 0.5) {\n    return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;\n  }\n  const sx = Math.sign(dx);\n  const sy = Math.sign(dy);\n  const sweep = sx * sy > 0 ? 1 : 0;\n  return [\n    `M ${from.x} ${from.y}`,\n    `L ${midX - sx * r} ${from.y}`,\n    `A ${r} ${r} 0 0 ${sweep} ${midX} ${from.y + sy * r}`,\n    `L ${midX} ${to.y - sy * r}`,\n    `A ${r} ${r} 0 0 ${1 - sweep} ${midX + sx * r} ${to.y}`,\n    `L ${to.x} ${to.y}`,\n  ].join(\" \");\n};\n\nexport const getFlowEdgePath = (\n  source: WorkflowAnchor,\n  target: WorkflowAnchor,\n  reverse = false,\n  offset = 0\n): [string, number, number] => {\n  const [from, to] = reverse ? [target, source] : [source, target];\n  if (target.x > source.x) {\n    return [\n      getEdgePath(from, to, WORKFLOW_EDGE_RADIUS, offset),\n      getEdgeMidX(from, to, WORKFLOW_EDGE_RADIUS, offset),\n      (source.y + target.y) / 2,\n    ];\n  }\n  const [path, labelX, labelY] = getSmoothStepPath({\n    ...WORKFLOW_EDGE_STEP_OPTIONS,\n    sourcePosition: from.position,\n    sourceX: from.x,\n    sourceY: from.y,\n    targetPosition: to.position,\n    targetX: to.x,\n    targetY: to.y,\n  });\n  return [path, labelX, labelY];\n};\n\nconst getHandleAnchor = (\n  state: ReactFlowState,\n  nodeId: string,\n  type: \"source\" | \"target\",\n  handleId?: string | null\n) => {\n  const node = state.nodeLookup.get(nodeId);\n  const handle = node?.internals.handleBounds?.[type]?.find(\n    (item) => item.id === handleId\n  );\n  if (!(node && handle)) {\n    return;\n  }\n  const { x, y } = node.internals.positionAbsolute;\n  return {\n    x: x + handle.x + (type === \"source\" ? handle.width : 0),\n    y: y + handle.y + handle.height / 2,\n  };\n};\n\nexport const getEdgeLaneOffset = (state: ReactFlowState, edgeId: string) => {\n  const edge = state.edgeLookup.get(edgeId);\n  if (!edge) {\n    return 0;\n  }\n  const lanes: WorkflowEdgeLane[] = [];\n  for (const item of state.edges) {\n    if (item.target !== edge.target) {\n      continue;\n    }\n    const from = getHandleAnchor(\n      state,\n      item.source,\n      \"source\",\n      item.sourceHandle\n    );\n    const to = getHandleAnchor(state, item.target, \"target\", item.targetHandle);\n    if (from && to && to.x > from.x && to.y !== from.y) {\n      lanes.push({ down: to.y > from.y, from, id: item.id, to });\n    }\n  }\n  const own = lanes.find((lane) => lane.id === edgeId);\n  if (!own) {\n    return 0;\n  }\n  const sign = own.down ? 1 : -1;\n  const group = lanes\n    .filter((lane) => lane.down === own.down)\n    .toSorted(\n      (a, b) => sign * (a.to.y - b.to.y) || sign * (a.from.y - b.from.y)\n    );\n  const rank = group.findIndex((lane) => lane.id === edgeId);\n  return ((group.length - 1) / 2 - rank) * WORKFLOW_EDGE_LANE_GAP;\n};\n\nexport const createNodeFromBlock = (\n  block: WorkflowBlock,\n  position: WorkflowPoint\n): WorkflowNodeData => ({\n  branch:\n    block.outputs && block.outputs.length > 1\n      ? block.outputs[0]?.id\n      : undefined,\n  description: block.description,\n  durationMs: block.durationMs,\n  id: `${block.id}-${Date.now().toString(36)}`,\n  inputs: block.inputs,\n  kind: block.kind,\n  logo: block.logo,\n  model: block.model,\n  outputs: block.outputs,\n  position,\n  title: block.title,\n});\n",
      "type": "registry:lib",
      "target": "components/workflow/lib/workflow.ts"
    },
    {
      "path": "registry/notra/workflow/types/workflow.ts",
      "content": "import type { Edge, Node, Position } from \"@xyflow/react\";\nimport type { ComponentProps, ComponentType, ReactNode, SVGProps } from \"react\";\n\nexport type WorkflowNodeKind =\n  | \"trigger\"\n  | \"task\"\n  | \"ai\"\n  | \"integration\"\n  | \"condition\"\n  | \"switch\"\n  | \"merge\"\n  | \"loop\"\n  | \"delay\"\n  | \"approval\"\n  | \"email\"\n  | \"output\";\n\nexport type WorkflowNodeStatus =\n  | \"idle\"\n  | \"queued\"\n  | \"running\"\n  | \"waiting\"\n  | \"success\"\n  | \"error\"\n  | \"skipped\";\n\nexport type WorkflowEdgeStatus = \"idle\" | \"active\" | \"done\" | \"skipped\";\n\nexport type WorkflowRunPhase = \"idle\" | \"running\" | \"done\";\n\nexport type WorkflowLogoName =\n  | \"openai\"\n  | \"anthropic\"\n  | \"claude\"\n  | \"gemini\"\n  | \"perplexity\"\n  | \"mistral\"\n  | \"deepseek\"\n  | \"grok\"\n  | \"browserbase\"\n  | \"context-dev\"\n  | \"firecrawl\"\n  | \"resend\"\n  | \"slack\"\n  | \"gmail\"\n  | \"notion\"\n  | \"google\"\n  | \"google-sheets\"\n  | \"webflow\"\n  | \"wordpress\"\n  | \"linear\"\n  | \"github\"\n  | \"ahrefs\"\n  | \"posthog\";\n\nexport type WorkflowLogLevel = \"info\" | \"success\" | \"error\" | \"skip\";\n\nexport type WorkflowIcon = ComponentType<SVGProps<SVGSVGElement>>;\n\nexport interface WorkflowPoint {\n  x: number;\n  y: number;\n}\n\nexport interface WorkflowAnchor extends WorkflowPoint {\n  position: Position;\n}\n\nexport interface WorkflowPort {\n  id: string;\n  label?: string;\n  hint?: string;\n  rate?: number;\n}\n\nexport interface WorkflowRule {\n  subject: string;\n  operator: string;\n  value: string;\n}\n\nexport interface WorkflowField {\n  label: string;\n  value: string;\n}\n\nexport interface WorkflowNodeData {\n  id: string;\n  kind: WorkflowNodeKind;\n  title: string;\n  description?: string;\n  position: WorkflowPoint;\n  inputs?: WorkflowPort[];\n  outputs?: WorkflowPort[];\n  logo?: WorkflowLogoName;\n  model?: string;\n  durationMs?: number;\n  branch?: string;\n  rule?: WorkflowRule;\n  fail?: boolean;\n  fields?: WorkflowField[];\n  output?: string;\n}\n\nexport interface WorkflowEdgeData {\n  id: string;\n  source: string;\n  sourcePort?: string;\n  target: string;\n  targetPort?: string;\n  label?: string;\n  animated?: boolean;\n}\n\nexport interface WorkflowEdgeLane {\n  id: string;\n  down: boolean;\n  from: WorkflowPoint;\n  to: WorkflowPoint;\n}\n\nexport interface WorkflowGraph {\n  edges: WorkflowEdgeData[];\n  nodes: WorkflowNodeData[];\n}\n\nexport interface WorkflowTemplate extends WorkflowGraph {\n  id: string;\n  title: string;\n  description: string;\n  category: string;\n}\n\nexport interface WorkflowKindConfig {\n  icon: WorkflowIcon;\n  label: string;\n  tileClassName: string;\n}\n\nexport interface WorkflowStatusConfig {\n  className: string;\n  label: string;\n  ledClassName: string;\n}\n\nexport interface WorkflowLogoConfig {\n  label: string;\n  logo: WorkflowIcon;\n  tile?: boolean;\n}\n\nexport interface WorkflowModel {\n  id: string;\n  label: string;\n  logo: WorkflowLogoName;\n  provider: string;\n}\n\nexport interface WorkflowBlock {\n  id: string;\n  title: string;\n  description: string;\n  group: string;\n  kind: WorkflowNodeKind;\n  logo?: WorkflowLogoName;\n  model?: string;\n  inputs?: WorkflowPort[];\n  outputs?: WorkflowPort[];\n  durationMs?: number;\n}\n\nexport interface WorkflowNodeRun {\n  start: number;\n  end: number;\n  status: \"success\" | \"error\" | \"skipped\";\n}\n\nexport interface WorkflowEdgeRun {\n  start: number;\n  end: number;\n  taken: boolean;\n}\n\nexport interface WorkflowSchedule {\n  edges: Record<string, WorkflowEdgeRun>;\n  nodes: Record<string, WorkflowNodeRun>;\n  order: string[];\n  totalMs: number;\n}\n\nexport interface WorkflowNodeState {\n  elapsedMs: number;\n  status: WorkflowNodeStatus;\n}\n\nexport interface WorkflowLogEntry {\n  at: number;\n  id: string;\n  level: WorkflowLogLevel;\n  message: string;\n  nodeId: string;\n  title: string;\n}\n\nexport interface WorkflowRunSummary {\n  id: string;\n  durationMs: number;\n  startedAt: string;\n  status: \"success\" | \"error\" | \"running\";\n  trigger: string;\n  steps: number;\n}\n\nexport interface WorkflowRun {\n  edgeState: (id: string) => WorkflowEdgeStatus;\n  elapsedMs: number;\n  nodeState: (id: string) => WorkflowNodeState;\n  phase: WorkflowRunPhase;\n  play: () => void;\n  schedule: WorkflowSchedule;\n  stop: () => void;\n  totalMs: number;\n}\n\nexport type WorkflowLogoProps = Omit<SVGProps<SVGSVGElement>, \"name\"> & {\n  name: WorkflowLogoName;\n};\n\nexport interface WorkflowFlowNodeData extends Record<string, unknown> {\n  node: WorkflowNodeData;\n  state?: WorkflowNodeState;\n}\n\nexport interface WorkflowFlowEdgeData extends Record<string, unknown> {\n  animated?: boolean;\n  reverse?: boolean;\n  label?: string;\n  status: WorkflowEdgeStatus;\n}\n\nexport type WorkflowFlowNode = Node<WorkflowFlowNodeData, \"workflow\">;\n\nexport type WorkflowFlowEdge = Edge<WorkflowFlowEdgeData, \"workflow\">;\n\nexport type WorkflowCanvasBackgroundVariant =\n  | \"dots\"\n  | \"lines\"\n  | \"cross\"\n  | \"grid\"\n  | \"none\";\n\nexport interface WorkflowCanvasBackgroundProps {\n  gap?: number;\n  variant?: WorkflowCanvasBackgroundVariant;\n}\n\nexport type WorkflowNodeSizes = Record<\n  string,\n  { height: number; width: number }\n>;\n\nexport interface WorkflowPortListProps {\n  ports: WorkflowPort[];\n  title: string;\n}\n\nexport interface WorkflowControlsProps {\n  toolbar?: ReactNode;\n}\n\nexport interface WorkflowBannerConfig {\n  className: string;\n  label: string;\n  message: string;\n}\n\nexport interface WorkflowStatusGlyphProps {\n  status: WorkflowNodeStatus;\n}\n\nexport interface WorkflowTemplatePreviewProps {\n  template: WorkflowTemplate;\n}\n\nexport interface WorkflowCanvasProps {\n  className?: string;\n  /** Follows new steps as they are added or move, like a workflow being written. */\n  autoFit?: boolean;\n  focusId?: string;\n  background?: WorkflowCanvasBackgroundVariant;\n  gap?: number;\n  nodes: WorkflowNodeData[];\n  edges: WorkflowEdgeData[];\n  nodeState?: (id: string) => WorkflowNodeState;\n  edgeState?: (id: string) => WorkflowEdgeStatus;\n  selectedId?: string;\n  controls?: boolean;\n  minimap?: boolean;\n  toolbar?: ReactNode;\n  minZoom?: number;\n  maxZoom?: number;\n  /** The smallest zoom that fits the whole graph; below it, the view shows the first or last steps instead. */\n  fitMinZoom?: number;\n  onSelectNode?: (id: string) => void;\n  onNodeMove?: (id: string, position: WorkflowPoint) => void;\n  onNodeRemove?: (id: string) => void;\n  onConnect?: (edge: Omit<WorkflowEdgeData, \"id\">) => void;\n  onDropBlock?: (blockId: string, position: WorkflowPoint) => void;\n}\n\nexport type WorkflowNodeCardProps = ComponentProps<\"div\"> & {\n  node: WorkflowNodeData;\n  state?: WorkflowNodeState;\n  selected?: boolean;\n  connectable?: boolean;\n  children?: ReactNode;\n};\n\nexport interface WorkflowHandleProps {\n  active?: boolean;\n  className?: string;\n  connectable?: boolean;\n  portId: string;\n  type: \"source\" | \"target\";\n}\n\nexport type WorkflowEdgeProps = ComponentProps<\"g\"> & {\n  path: string;\n  reverse?: boolean;\n  status?: WorkflowEdgeStatus;\n  animated?: boolean;\n};\n\nexport type WorkflowTimerProps = ComponentProps<\"span\"> & {\n  ms: number;\n  running?: boolean;\n};\n\nexport type WorkflowStatusIndicatorProps = ComponentProps<\"span\"> & {\n  status: WorkflowNodeStatus;\n};\n\nexport type WorkflowStatusBadgeProps = ComponentProps<\"span\"> & {\n  status: WorkflowNodeStatus;\n};\n\nexport type WorkflowStatusPillProps = ComponentProps<\"span\"> & {\n  status: WorkflowNodeStatus;\n};\n\nexport type WorkflowDeterminismBadgeProps = ComponentProps<\"span\"> & {\n  deterministic: boolean;\n};\n\nexport type WorkflowRunButtonProps = Omit<\n  ComponentProps<\"button\">,\n  \"children\"\n> & {\n  phase: WorkflowRunPhase;\n  progress?: number;\n  onPlay?: () => void;\n  onStop?: () => void;\n};\n\nexport type WorkflowNodePaletteProps = ComponentProps<\"div\"> & {\n  blocks: WorkflowBlock[];\n  onAdd?: (block: WorkflowBlock) => void;\n};\n\nexport interface WorkflowNodeBranchProps {\n  connectable?: boolean;\n  node: WorkflowNodeData;\n  port: WorkflowPort;\n  status: WorkflowNodeStatus;\n}\n\nexport interface WorkflowNodeBannerProps {\n  status: WorkflowNodeStatus;\n}\n\nexport interface WorkflowNodeSummaryProps {\n  children?: ReactNode;\n  node: WorkflowNodeData;\n}\n\nexport interface WorkflowNodeEngineProps {\n  node: WorkflowNodeData;\n}\n\nexport interface WorkflowNodeFooterProps {\n  node: WorkflowNodeData;\n  state?: WorkflowNodeState;\n}\n\nexport interface WorkflowBranchSelectProps {\n  node: WorkflowNodeData;\n  outputs: WorkflowPort[];\n  onBranchChange: (id: string, branch: string) => void;\n}\n\nexport type WorkflowNodeSettingsProps = Pick<\n  WorkflowNodeSheetProps,\n  \"onBranchChange\" | \"onModelChange\"\n> & {\n  node: WorkflowNodeData;\n};\n\nexport type WorkflowNodeSheetProps = WorkflowOverlayProps & {\n  node?: WorkflowNodeData;\n  state?: WorkflowNodeState;\n  log?: WorkflowLogEntry[];\n  onDelete?: (id: string) => void;\n  onModelChange?: (id: string, model: string) => void;\n  onBranchChange?: (id: string, branch: string) => void;\n};\n\nexport type WorkflowRunLogProps = ComponentProps<\"div\"> & {\n  entries: WorkflowLogEntry[];\n  emptyLabel?: string;\n};\n\nexport type WorkflowRunTimelineProps = ComponentProps<\"div\"> & {\n  nodes: WorkflowNodeData[];\n  schedule: WorkflowSchedule;\n  elapsedMs: number;\n  phase: WorkflowRunPhase;\n};\n\nexport type WorkflowRunHistoryProps = Omit<\n  ComponentProps<\"div\">,\n  \"onSelect\"\n> & {\n  runs: WorkflowRunSummary[];\n  selectedId?: string;\n  onSelect?: (id: string) => void;\n};\n\nexport interface WorkflowModelSelectProps {\n  className?: string;\n  defaultValue?: string;\n  disabled?: boolean;\n  id?: string;\n  models?: WorkflowModel[];\n  onValueChange?: (value: string) => void;\n  value?: string;\n}\n\nexport type WorkflowTemplateCardProps = ComponentProps<\"div\"> & {\n  template: WorkflowTemplate;\n  onUse?: (template: WorkflowTemplate) => void;\n};\n\nexport type WorkflowTemplateDialogProps = WorkflowOverlayProps & {\n  template?: WorkflowTemplate;\n  onUse?: (template: WorkflowTemplate) => void;\n};\n\nexport interface WorkflowBlockIconProps {\n  block: Pick<WorkflowBlock, \"kind\" | \"logo\" | \"model\">;\n  className?: string;\n}\n\nexport interface WorkflowOverlayProps {\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n}\n\nexport interface WorkflowCanvasBackgroundOption {\n  label: string;\n  value: WorkflowCanvasBackgroundVariant;\n}\n\nexport interface WorkflowNodeDemo {\n  node: WorkflowNodeData;\n  state: WorkflowNodeState;\n}\n\nexport interface WorkflowEdgeDemo {\n  animated?: boolean;\n  label: string;\n  status: WorkflowEdgeStatus;\n}\n",
      "type": "registry:lib",
      "target": "components/workflow/types/workflow.ts"
    }
  ],
  "type": "registry:component"
}