{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "workflow-canvas",
  "type": "registry:component",
  "title": "Workflow canvas",
  "description": "An interactive graph with draggable nodes, connections, viewport controls, anchored panels, and node actions.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/canvas-controls.json",
    "https://vlak.dev/r/toolbar.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex",
    "@xyflow/react"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.\nImport the copied styles/vlak/workflow.css stylesheet in your application.",
  "files": [
    {
      "path": "vlak/workflow-canvas.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { Background, BaseEdge, Handle, NodeToolbar, Panel, Position, ReactFlow, getBezierPath, useReactFlow, useStore, useStoreApi, useViewport, type ConnectionLineComponentProps, type Edge, type EdgeProps, type Node, type NodeProps, type NodeToolbarProps, type PanelProps, type ReactFlowProps } from \"@xyflow/react\";\nimport { rs } from \"./rs\";\nimport { mq, vlak } from \"./tokens.stylex\";\nimport { Button } from \"./button\";\nimport { CanvasControls } from \"./canvas-controls\";\nimport { Toolbar, type ToolbarProps } from \"./toolbar\";\n\nexport type WorkflowNodeData = Record<string, unknown> & { title: string; description?: React.ReactNode; content?: React.ReactNode; footer?: React.ReactNode; source?: boolean; target?: boolean };\nexport type WorkflowGraphNode = Node<WorkflowNodeData, \"vlak\">;\nexport interface WorkflowNodeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  footer?: React.ReactNode;\n  actions?: React.ReactNode;\n  source?: boolean;\n  target?: boolean;\n  sourcePosition?: Position;\n  targetPosition?: Position;\n  selected?: boolean;\n  connectable?: boolean;\n}\n\nconst dash = stylex.keyframes({ to: { strokeDashoffset: -20 } });\nconst styles = stylex.create({\n  canvas: {\n    width: \"100%\", height: \"32rem\", minHeight: \"16rem\", minWidth: 0, borderWidth: 1, borderStyle: \"solid\", borderColor: vlak.divider, borderRadius: vlak.radiusSm, overflow: \"hidden\", backgroundColor: vlak.paper, color: vlak.ink,\n    // Engine surfaces inherit the same palette as Vlak nodes, including generic edges and arrow markers.\n    \"--xy-background-color\": { default: vlak.paper, [mq.forcedColors]: \"Canvas\" },\n    \"--xy-background-pattern-color\": { default: vlak.divider, [mq.forcedColors]: \"GrayText\" },\n    \"--xy-edge-stroke\": { default: vlak.gray, [mq.forcedColors]: \"CanvasText\" },\n    \"--xy-edge-stroke-selected\": { default: vlak.ink, [mq.forcedColors]: \"Highlight\" },\n    \"--xy-connectionline-stroke\": { default: vlak.gray, [mq.forcedColors]: \"CanvasText\" },\n    \"--xy-handle-background-color\": { default: vlak.ink, [mq.forcedColors]: \"CanvasText\" },\n    \"--xy-node-color\": { default: vlak.ink, [mq.forcedColors]: \"CanvasText\" },\n    \"--xy-node-border\": { default: `1px solid ${vlak.controlBorder}`, [mq.forcedColors]: \"1px solid CanvasText\" },\n    \"--xy-node-border-selected\": { default: `1px solid ${vlak.ink}`, [mq.forcedColors]: \"1px solid Highlight\" },\n    \"--xy-selection-background-color\": { default: `color-mix(in srgb, ${vlak.ink} 10%, transparent)`, [mq.forcedColors]: \"transparent\" },\n    \"--xy-selection-border\": { default: `1px solid ${vlak.controlBorder}`, [mq.forcedColors]: \"1px solid Highlight\" },\n    \"--xy-resize-background-color\": { default: vlak.ink, [mq.forcedColors]: \"Highlight\" },\n    \"--xy-attribution-background-color\": { default: vlak.paper, [mq.forcedColors]: \"Canvas\" },\n    \"--xy-attribution-color\": { default: vlak.gray, [mq.forcedColors]: \"LinkText\" },\n    \"--xy-minimap-background-color\": { default: vlak.paper, [mq.forcedColors]: \"Canvas\" },\n    \"--xy-minimap-mask-background-color\": { default: `color-mix(in srgb, ${vlak.ink} 8%, transparent)`, [mq.forcedColors]: \"transparent\" },\n    \"--xy-minimap-mask-stroke-color\": { default: vlak.gray, [mq.forcedColors]: \"CanvasText\" },\n    \"--xy-minimap-node-background-color\": { default: vlak.controlFill, [mq.forcedColors]: \"Canvas\" },\n    \"--xy-minimap-node-stroke-color\": { default: vlak.controlBorder, [mq.forcedColors]: \"CanvasText\" },\n  },\n  node: { position: \"relative\", boxSizing: \"border-box\", width: \"18rem\", maxWidth: \"100%\", backgroundColor: vlak.paper, color: vlak.ink, borderWidth: 1, borderStyle: \"solid\", borderColor: { default: vlak.controlBorder, [mq.forcedColors]: \"CanvasText\" }, borderRadius: vlak.radiusSm },\n  selected: { outlineWidth: 2, outlineStyle: \"solid\", outlineColor: vlak.ink, outlineOffset: 3 },\n  header: { display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: \"0.75rem\", padding: \"1rem\" },\n  title: { fontSize: \"0.875rem\", fontWeight: 600 },\n  description: { marginTop: \"0.25rem\", color: vlak.gray, fontSize: \"0.8125rem\", lineHeight: 1.45 },\n  content: { padding: \"1rem\", borderTopWidth: 1, borderTopStyle: \"solid\", borderTopColor: vlak.divider, fontSize: \"0.875rem\", lineHeight: 1.45 },\n  footer: { padding: \"0.75rem 1rem\", borderTopWidth: 1, borderTopStyle: \"solid\", borderTopColor: vlak.divider, fontSize: \"0.8125rem\", color: vlak.gray },\n  handle: {\n    // React Flow measures this box to anchor edges; the pseudo-element expands only pointer hit testing.\n    width: 8, height: 8, minWidth: 8, minHeight: 8, borderWidth: 0, borderRadius: \"50%\", backgroundColor: \"transparent\", cursor: \"crosshair\",\n    \"::before\": { content: '\"\"', position: \"absolute\", inset: `calc(50% - ${vlak.hit} / 2)`, width: vlak.hit, height: vlak.hit, pointerEvents: \"inherit\" },\n  },\n  dot: { display: \"block\", position: \"absolute\", inset: \"calc(50% - 4px)\", borderRadius: \"50%\", backgroundColor: vlak.ink, pointerEvents: \"none\" },\n  // Let React Flow's focus/selection selectors own stroke so lower engine layers retain keyboard feedback.\n  edge: { \"--xy-edge-stroke\": { default: vlak.gray, [mq.forcedColors]: \"CanvasText\" }, \"--xy-edge-stroke-selected\": { default: vlak.ink, [mq.forcedColors]: \"Highlight\" }, \"--xy-edge-stroke-width\": \"1.5\", \"--xy-connectionline-stroke\": { default: vlak.gray, [mq.forcedColors]: \"CanvasText\" }, \"--xy-connectionline-stroke-width\": \"1.5\", fill: \"none\" },\n  temporary: { strokeDasharray: \"5 5\" },\n  animated: { strokeDasharray: \"5 5\", animationName: { default: dash, [mq.reduce]: \"none\" }, animationDuration: \"1s\", animationTimingFunction: \"linear\", animationIterationCount: \"infinite\" },\n  endpoint: { fill: vlak.paper, stroke: vlak.ink, strokeWidth: 1.5 },\n  panel: { padding: \"0.25rem\", borderWidth: 1, borderStyle: \"solid\", borderColor: vlak.divider, borderRadius: vlak.radiusSm, backgroundColor: vlak.paper, color: vlak.ink, maxWidth: \"calc(100% - 24px)\" },\n  controls: { display: \"flex\", alignItems: \"center\", flexWrap: \"wrap\", gap: \"0.25rem\" },\n});\n\n/** A graph node surface. Render inside React Flow so its handles use the graph's geometry. */\nexport const WorkflowNode = React.forwardRef<HTMLDivElement, WorkflowNodeProps>(function WorkflowNode({ title, description, children, footer, actions, source = true, target = true, sourcePosition = Position.Right, targetPosition = Position.Left, selected = false, connectable = true, className, style, ...props }, ref) {\n  const root = rs([\"rs-workflow-canvas-node\", selected && \"rs-workflow-canvas-selected\", className], styles.node, selected && styles.selected);\n  const handle = rs([\"rs-workflow-canvas-handle\"], styles.handle);\n  const dot = rs([\"rs-workflow-canvas-dot\"], styles.dot);\n  return <div ref={ref} {...props} className={root.className} style={{ ...root.style, ...style }}>\n    {target && <Handle {...handle} type=\"target\" position={targetPosition} isConnectable={connectable} aria-hidden=\"true\"><span {...dot} /></Handle>}\n    <header {...rs([\"rs-workflow-canvas-header\"], styles.header)}><div><div {...rs([\"rs-workflow-canvas-title\"], styles.title)}>{title}</div>{description && <div {...rs([\"rs-workflow-canvas-description\"], styles.description)}>{description}</div>}</div>{actions && <div className=\"nodrag nopan nokey\">{actions}</div>}</header>\n    {children && <div {...rs([\"rs-workflow-canvas-content\"], styles.content)}>{children}</div>}\n    {footer && <footer {...rs([\"rs-workflow-canvas-footer\"], styles.footer)}>{footer}</footer>}\n    {source && <Handle {...handle} type=\"source\" position={sourcePosition} isConnectable={connectable} aria-hidden=\"true\"><span {...dot} /></Handle>}\n  </div>;\n});\n\nfunction DefaultNode({ data, selected, isConnectable, sourcePosition, targetPosition }: NodeProps<WorkflowGraphNode>) {\n  return <WorkflowNode title={data.title} description={data.description} footer={data.footer} source={data.source} target={data.target} sourcePosition={sourcePosition} targetPosition={targetPosition} selected={selected} connectable={isConnectable}>{data.content}</WorkflowNode>;\n}\n\n/** Bezier edge with engine-managed hit testing and optional temporary or animated state. */\nexport function WorkflowEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerStart, markerEnd, style, selected, animated, data, label, labelStyle, interactionWidth }: EdgeProps<Edge<{ temporary?: boolean }>>) {\n  const [path, labelX, labelY] = getBezierPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });\n  const temporary = !!data?.temporary;\n  const moving = !!animated;\n  const paint = rs([\"rs-workflow-canvas-edge\", temporary && \"rs-workflow-canvas-temporary\", moving && \"rs-workflow-canvas-animated\"], styles.edge, temporary && styles.temporary, moving && styles.animated);\n  return <BaseEdge id={id} path={path} markerStart={markerStart} markerEnd={markerEnd} label={label} labelX={labelX} labelY={labelY} labelStyle={labelStyle} interactionWidth={interactionWidth} className={paint.className} style={{ ...paint.style, ...(selected ? { strokeWidth: 2 } : {}), ...style }} />;\n}\n\n/** Pointer-following connection preview while creating an edge. */\nexport function WorkflowConnection({ fromX, fromY, toX, toY, fromPosition, toPosition, connectionStatus }: ConnectionLineComponentProps) {\n  const [path] = getBezierPath({ sourceX: fromX, sourceY: fromY, targetX: toX, targetY: toY, sourcePosition: fromPosition, targetPosition: toPosition });\n  const paint = rs([\"rs-workflow-canvas-edge\", \"rs-workflow-canvas-temporary\"], styles.edge, styles.temporary);\n  // biome-ignore lint/a11y/noAriaHiddenOnFocusable: The decorative SVG group explicitly disables focus.\n  return <g aria-hidden=\"true\" focusable=\"false\" data-status={connectionStatus}><path {...paint} className={`react-flow__connection-path ${paint.className ?? \"\"}`} d={path} /><circle {...rs([\"rs-workflow-canvas-endpoint\"], styles.endpoint)} cx={toX} cy={toY} r={4} /></g>;\n}\n\nconst nodeTypes = { vlak: DefaultNode };\nconst edgeTypes = { vlak: WorkflowEdge };\n\n/** Optional React Flow adapter. Supply a bounded height and controlled changes or defaultNodes/defaultEdges. */\nexport function WorkflowCanvas<N extends Node = WorkflowGraphNode, E extends Edge = Edge>({ children, className, style, ...props }: ReactFlowProps<N, E>) {\n  const root = rs([\"rs-workflow-canvas\", className], styles.canvas);\n  return <div className={root.className} style={{ ...root.style, ...style }}><ReactFlow<N, E> nodeTypes={nodeTypes} edgeTypes={edgeTypes} defaultEdgeOptions={{ type: \"vlak\" }} connectionLineComponent={WorkflowConnection} deleteKeyCode={[\"Backspace\", \"Delete\"]} panOnScroll selectionOnDrag panOnDrag={false} zoomOnDoubleClick={false} fitView {...props}>\n    <Background color=\"var(--divider)\" bgColor=\"var(--bg)\" gap={20} size={1} />{children}\n  </ReactFlow></div>;\n}\n\n/** Overlay anchored to one of React Flow's six viewport positions. */\nexport function WorkflowPanel({ className, style, ...props }: PanelProps) {\n  const root = rs([\"rs-workflow-canvas-panel\", className], styles.panel);\n  return <Panel {...props} className={root.className} style={{ ...root.style, ...style }} />;\n}\n\nexport interface WorkflowControlsProps extends Omit<PanelProps, \"children\"> { label?: string }\n/** Binds Vlak's canvas controls to the graph viewport and interaction lock. */\nexport function WorkflowControls({ label = \"Workflow controls\", position = \"bottom-left\", ...props }: WorkflowControlsProps) {\n  const flow = useReactFlow();\n  const { zoom } = useViewport();\n  const minZoom = useStore(state => state.minZoom);\n  const maxZoom = useStore(state => state.maxZoom);\n  const locked = useStore(state => !state.nodesDraggable && !state.nodesConnectable && !state.elementsSelectable);\n  return <WorkflowPanel position={position} {...props}><div {...rs([\"rs-workflow-canvas-controls\"], styles.controls)}>\n    <CanvasControls label={label} zoom={zoom} minZoom={minZoom} maxZoom={maxZoom} onZoomChange={value => { void flow.zoomTo(value); }} onFit={() => { void flow.fitView(); }} onReset={() => { void flow.setViewport({ x: 0, y: 0, zoom: 1 }); }} />\n    <WorkflowInteractionLock locked={locked} />\n  </div></WorkflowPanel>;\n}\n\n// React Flow exposes its provider store for interaction controls; node positions remain caller-owned.\nfunction WorkflowInteractionLock({ locked }: { locked: boolean }) {\n  const store = useStoreApi();\n  const previous = React.useRef<{ nodesDraggable: boolean; nodesConnectable: boolean; elementsSelectable: boolean } | undefined>(undefined);\n  return <Button variant=\"subtle\" aria-label={locked ? \"Unlock workflow\" : \"Lock workflow\"} aria-pressed={locked} onClick={() => {\n    if (locked) store.setState(previous.current ?? { nodesDraggable: true, nodesConnectable: true, elementsSelectable: true });\n    else { const state = store.getState(); previous.current = { nodesDraggable: state.nodesDraggable, nodesConnectable: state.nodesConnectable, elementsSelectable: state.elementsSelectable }; store.setState({ nodesDraggable: false, nodesConnectable: false, elementsSelectable: false }); }\n  }}>{locked ? \"Unlock\" : \"Lock\"}</Button>;\n}\n\nexport interface WorkflowToolbarProps extends Omit<NodeToolbarProps, \"children\"> { actions: ToolbarProps[\"actions\"]; label: string }\n/** Node-anchored toolbar with Vlak's arrow-key action navigation. */\nexport function WorkflowToolbar({ actions, label, className, style, ...props }: WorkflowToolbarProps) {\n  const root = rs([\"rs-workflow-canvas-panel\", \"nokey\", className], styles.panel);\n  return <NodeToolbar {...props} className={root.className} style={{ ...root.style, ...style }}><Toolbar label={label} actions={actions} /></NodeToolbar>;\n}\n",
      "type": "registry:component",
      "target": "components/vlak/workflow-canvas.tsx"
    },
    {
      "path": "vlak/styles/workflow-canvas.css",
      "content": "/* ── workflow-canvas: generated from packages/react/src/components/workflow-canvas.tsx ── */\n@keyframes rs-anim-dash{to{stroke-dashoffset:-20}}\n.rs-workflow-canvas{width:100%;height:32rem;min-height:16rem;min-width:0;border-width:1px;border-style:solid;border-color:var(--divider);border-radius:var(--radius-sm);overflow:hidden;background-color:var(--bg);color:var(--text);--xy-background-color:var(--bg);--xy-background-pattern-color:var(--divider);--xy-edge-stroke:var(--text-secondary);--xy-edge-stroke-selected:var(--text);--xy-connectionline-stroke:var(--text-secondary);--xy-handle-background-color:var(--text);--xy-node-color:var(--text);--xy-node-border:1px solid var(--control-border);--xy-node-border-selected:1px solid var(--text);--xy-selection-background-color:color-mix(in srgb, var(--text) 10%, transparent);--xy-selection-border:1px solid var(--control-border);--xy-resize-background-color:var(--text);--xy-attribution-background-color:var(--bg);--xy-attribution-color:var(--text-secondary);--xy-minimap-background-color:var(--bg);--xy-minimap-mask-background-color:color-mix(in srgb, var(--text) 8%, transparent);--xy-minimap-mask-stroke-color:var(--text-secondary);--xy-minimap-node-background-color:var(--control-fill);--xy-minimap-node-stroke-color:var(--control-border)}\n@media (forced-colors: active){.rs-workflow-canvas{--xy-background-color:Canvas;--xy-background-pattern-color:GrayText;--xy-edge-stroke:CanvasText;--xy-edge-stroke-selected:Highlight;--xy-connectionline-stroke:CanvasText;--xy-handle-background-color:CanvasText;--xy-node-color:CanvasText;--xy-node-border:1px solid CanvasText;--xy-node-border-selected:1px solid Highlight;--xy-selection-background-color:transparent;--xy-selection-border:1px solid Highlight;--xy-resize-background-color:Highlight;--xy-attribution-background-color:Canvas;--xy-attribution-color:LinkText;--xy-minimap-background-color:Canvas;--xy-minimap-mask-background-color:transparent;--xy-minimap-mask-stroke-color:CanvasText;--xy-minimap-node-background-color:Canvas;--xy-minimap-node-stroke-color:CanvasText}}\n.rs-workflow-canvas-node{position:relative;box-sizing:border-box;width:18rem;max-width:100%;background-color:var(--bg);color:var(--text);border-width:1px;border-style:solid;border-color:var(--control-border);border-radius:var(--radius-sm)}\n@media (forced-colors: active){.rs-workflow-canvas-node{border-color:CanvasText}}\n.rs-workflow-canvas-selected{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:3px}\n.rs-workflow-canvas-header{display:flex;align-items:center;justify-content:space-between;gap:0.75rem;padding:1rem}\n.rs-workflow-canvas-title{font-size:0.875rem;font-weight:600}\n.rs-workflow-canvas-description{margin-top:0.25rem;color:var(--text-secondary);font-size:0.8125rem;line-height:1.45}\n.rs-workflow-canvas-content{padding:1rem;border-top-width:1px;border-top-style:solid;border-top-color:var(--divider);font-size:0.875rem;line-height:1.45}\n.rs-workflow-canvas-footer{padding:0.75rem 1rem;border-top-width:1px;border-top-style:solid;border-top-color:var(--divider);font-size:0.8125rem;color:var(--text-secondary)}\n.rs-workflow-canvas-handle{width:8px;height:8px;min-width:8px;min-height:8px;border-width:0;border-radius:50%;background-color:transparent;cursor:crosshair}\n.rs-workflow-canvas-handle::before{content:\"\";position:absolute;inset:calc(50% - var(--hit) / 2);width:var(--hit);height:var(--hit);pointer-events:inherit}\n.rs-workflow-canvas-dot{display:block;position:absolute;inset:calc(50% - 4px);border-radius:50%;background-color:var(--text);pointer-events:none}\n.rs-workflow-canvas-edge{--xy-edge-stroke:var(--text-secondary);--xy-edge-stroke-selected:var(--text);--xy-edge-stroke-width:1.5;--xy-connectionline-stroke:var(--text-secondary);--xy-connectionline-stroke-width:1.5;fill:none}\n@media (forced-colors: active){.rs-workflow-canvas-edge{--xy-edge-stroke:CanvasText;--xy-edge-stroke-selected:Highlight;--xy-connectionline-stroke:CanvasText}}\n.rs-workflow-canvas-temporary{stroke-dasharray:5 5}\n.rs-workflow-canvas-animated{stroke-dasharray:5 5;animation-name:rs-anim-dash;animation-duration:1s;animation-timing-function:linear;animation-iteration-count:infinite}\n@media (prefers-reduced-motion: reduce){.rs-workflow-canvas-animated{animation-name:none}}\n.rs-workflow-canvas-endpoint{fill:var(--bg);stroke:var(--text);stroke-width:1.5}\n.rs-workflow-canvas-panel,.rs-workflow-canvas-panel.nokey{padding:0.25rem;border-width:1px;border-style:solid;border-color:var(--divider);border-radius:var(--radius-sm);background-color:var(--bg);color:var(--text);max-width:calc(100% - 24px)}\n.rs-workflow-canvas-controls{display:flex;align-items:center;flex-wrap:wrap;gap:0.25rem}\n",
      "type": "registry:file",
      "target": "styles/vlak/workflow-canvas.css"
    },
    {
      "path": "vlak/styles/workflow.css",
      "content": "/* Engine layout stays below Vlak's component paint and consumer overrides. */\n@layer vlak.engine, vlak.tokens, vlak.base, vlak.type, vlak.components, vlak.touch, vlak.motion;\n/*! @xyflow/react 12.11.6 — https://reactflow.dev\nMIT License\n\nCopyright (c) 2019-2025 webkid GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\n@layer vlak.engine {\n/* this will be exported as base.css and can be used for a basic styling */\n/* these are the necessary styles for React/Svelte Flow, they get used by base.css and style.css */\n.react-flow {\n  direction: ltr;\n\n  --xy-edge-stroke-default: #b1b1b7;\n  --xy-edge-stroke-width-default: 1;\n  --xy-edge-stroke-selected-default: #555;\n\n  --xy-connectionline-stroke-default: #b1b1b7;\n  --xy-connectionline-stroke-width-default: 1;\n\n  --xy-attribution-background-color-default: rgba(255, 255, 255, 0.5);\n\n  --xy-minimap-background-color-default: #fff;\n  --xy-minimap-mask-background-color-default: rgba(240, 240, 240, 0.6);\n  --xy-minimap-mask-stroke-color-default: transparent;\n  --xy-minimap-mask-stroke-width-default: 1;\n  --xy-minimap-node-background-color-default: #e2e2e2;\n  --xy-minimap-node-stroke-color-default: transparent;\n  --xy-minimap-node-stroke-width-default: 2;\n\n  --xy-background-color-default: transparent;\n  --xy-background-pattern-dots-color-default: #91919a;\n  --xy-background-pattern-lines-color-default: #eee;\n  --xy-background-pattern-cross-color-default: #e2e2e2;\n  background-color: var(--xy-background-color, var(--xy-background-color-default));\n  --xy-node-border-default: 1px solid #bbb;\n  --xy-node-border-selected-default: 1px solid #555;\n\n  --xy-handle-background-color-default: #333;\n\n  --xy-selection-background-color-default: rgba(150, 150, 180, 0.1);\n  --xy-selection-border-default: 1px dotted rgba(155, 155, 155, 0.8);\n  --xy-resize-background-color-default: #3367d9;\n}\n.react-flow.dark {\n  --xy-edge-stroke-default: #3e3e3e;\n  --xy-edge-stroke-width-default: 1;\n  --xy-edge-stroke-selected-default: #727272;\n\n  --xy-connectionline-stroke-default: #b1b1b7;\n  --xy-connectionline-stroke-width-default: 1;\n\n  --xy-attribution-background-color-default: rgba(150, 150, 150, 0.25);\n\n  --xy-minimap-background-color-default: #141414;\n  --xy-minimap-mask-background-color-default: rgba(60, 60, 60, 0.6);\n  --xy-minimap-mask-stroke-color-default: transparent;\n  --xy-minimap-mask-stroke-width-default: 1;\n  --xy-minimap-node-background-color-default: #2b2b2b;\n  --xy-minimap-node-stroke-color-default: transparent;\n  --xy-minimap-node-stroke-width-default: 2;\n\n  --xy-background-color-default: #141414;\n  --xy-background-pattern-dots-color-default: #555;\n  --xy-background-pattern-lines-color-default: #333;\n  --xy-background-pattern-cross-color-default: #333;\n  --xy-node-color-default: #f8f8f8;\n}\n.react-flow__background {\n  background-color: var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));\n  pointer-events: none;\n  z-index: -1;\n}\n.react-flow__container {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n}\n.react-flow__pane {\n  z-index: 1;\n  touch-action: none;\n}\n.react-flow__pane.draggable {\n    cursor: grab;\n  }\n.react-flow__pane.dragging {\n    cursor: grabbing;\n  }\n.react-flow__pane.selection {\n    cursor: pointer;\n  }\n.react-flow__viewport {\n  transform-origin: 0 0;\n  z-index: 2;\n  pointer-events: none;\n}\n.react-flow__renderer {\n  z-index: 4;\n}\n.react-flow__selection {\n  z-index: 6;\n}\n.react-flow__nodesselection-rect:focus,\n.react-flow__nodesselection-rect:focus-visible {\n  outline: none;\n}\n.react-flow__edge-path {\n  stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));\n  stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));\n  fill: none;\n}\n.react-flow__connection-path {\n  stroke: var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));\n  stroke-width: var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));\n  fill: none;\n}\n.react-flow .react-flow__edges {\n  position: absolute;\n}\n.react-flow .react-flow__edges svg {\n    overflow: visible;\n    position: absolute;\n    pointer-events: none;\n  }\n.react-flow__edge {\n  pointer-events: visibleStroke;\n}\n.react-flow__edge.selectable {\n    cursor: pointer;\n  }\n.react-flow__edge.animated path {\n    stroke-dasharray: 5;\n    animation: dashdraw 0.5s linear infinite;\n  }\n.react-flow__edge.animated path.react-flow__edge-interaction {\n    stroke-dasharray: none;\n    animation: none;\n  }\n.react-flow__edge.inactive {\n    pointer-events: none;\n  }\n.react-flow__edge.selected,\n  .react-flow__edge:focus,\n  .react-flow__edge:focus-visible {\n    outline: none;\n  }\n.react-flow__edge.selected .react-flow__edge-path,\n  .react-flow__edge.selectable:focus .react-flow__edge-path,\n  .react-flow__edge.selectable:focus-visible .react-flow__edge-path {\n    stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default));\n  }\n.react-flow__edge-textwrapper {\n    pointer-events: all;\n  }\n.react-flow__edge .react-flow__edge-text {\n    pointer-events: none;\n    -webkit-user-select: none;\n       -moz-user-select: none;\n            user-select: none;\n  }\n/* Arrowhead marker styles - use CSS custom properties as default */\n.react-flow__arrowhead polyline {\n  stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default));\n}\n.react-flow__arrowhead polyline.arrowclosed {\n  fill: var(--xy-edge-stroke, var(--xy-edge-stroke-default));\n}\n.react-flow__connection {\n  pointer-events: none;\n}\n.react-flow__connection .animated {\n    stroke-dasharray: 5;\n    animation: dashdraw 0.5s linear infinite;\n  }\nsvg.react-flow__connectionline {\n  z-index: 1001;\n  overflow: visible;\n  position: absolute;\n}\n.react-flow__nodes {\n  pointer-events: none;\n  transform-origin: 0 0;\n}\n.react-flow__node {\n  position: absolute;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  pointer-events: all;\n  transform-origin: 0 0;\n  box-sizing: border-box;\n  cursor: default;\n}\n.react-flow__node.selectable {\n    cursor: pointer;\n  }\n.react-flow__node.draggable {\n    cursor: grab;\n    pointer-events: all;\n  }\n.react-flow__node.draggable.dragging {\n      cursor: grabbing;\n    }\n.react-flow__nodesselection {\n  z-index: 3;\n  transform-origin: left top;\n  pointer-events: none;\n}\n.react-flow__nodesselection-rect {\n    position: absolute;\n    pointer-events: all;\n    cursor: grab;\n  }\n.react-flow__handle {\n  position: absolute;\n  pointer-events: none;\n  min-width: 5px;\n  min-height: 5px;\n  background-color: var(--xy-handle-background-color, var(--xy-handle-background-color-default));\n}\n.react-flow__handle.connectingfrom {\n    pointer-events: all;\n  }\n.react-flow__handle.connectionindicator {\n    pointer-events: all;\n    cursor: crosshair;\n  }\n.react-flow__handle-bottom {\n    top: auto;\n    left: 50%;\n    bottom: 0;\n    transform: translate(-50%, 50%);\n  }\n.react-flow__handle-top {\n    top: 0;\n    left: 50%;\n    transform: translate(-50%, -50%);\n  }\n.react-flow__handle-left {\n    top: 50%;\n    left: 0;\n    transform: translate(-50%, -50%);\n  }\n.react-flow__handle-right {\n    top: 50%;\n    right: 0;\n    transform: translate(50%, -50%);\n  }\n.react-flow__edgeupdater {\n  cursor: move;\n  pointer-events: all;\n}\n.react-flow__pane.selection .react-flow__panel {\n  pointer-events: none;\n}\n.react-flow__panel {\n  position: absolute;\n  z-index: 5;\n  margin: 15px;\n}\n.react-flow__panel.top {\n    top: 0;\n  }\n.react-flow__panel.bottom {\n    bottom: 0;\n  }\n.react-flow__panel.top.center, .react-flow__panel.bottom.center {\n      left: 50%;\n      transform: translateX(-15px) translateX(-50%);\n    }\n.react-flow__panel.left {\n    left: 0;\n  }\n.react-flow__panel.right {\n    right: 0;\n  }\n.react-flow__panel.left.center, .react-flow__panel.right.center {\n      top: 50%;\n      transform: translateY(-15px) translateY(-50%);\n    }\n.react-flow__attribution {\n  font-size: 10px;\n  background: var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));\n  padding: 2px 3px;\n  margin: 0;\n}\n.react-flow__attribution a {\n    text-decoration: none;\n    color: var(--xy-attribution-color, #999);\n  }\n@keyframes dashdraw {\n  from {\n    stroke-dashoffset: 10;\n  }\n}\n.react-flow__edgelabel-renderer {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  left: 0;\n  top: 0;\n}\n.react-flow__viewport-portal {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n.react-flow__minimap {\n  background: var(\n    --xy-minimap-background-color-props,\n    var(--xy-minimap-background-color, var(--xy-minimap-background-color-default))\n  );\n}\n.react-flow__minimap-svg {\n    display: block;\n  }\n.react-flow__minimap-mask {\n    fill: var(\n      --xy-minimap-mask-background-color-props,\n      var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default))\n    );\n    stroke: var(\n      --xy-minimap-mask-stroke-color-props,\n      var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default))\n    );\n    stroke-width: var(\n      --xy-minimap-mask-stroke-width-props,\n      var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default))\n    );\n  }\n.react-flow__minimap-node {\n    fill: var(\n      --xy-minimap-node-background-color-props,\n      var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default))\n    );\n    stroke: var(\n      --xy-minimap-node-stroke-color-props,\n      var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default))\n    );\n    stroke-width: var(\n      --xy-minimap-node-stroke-width-props,\n      var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default))\n    );\n  }\n.react-flow__background-pattern.dots {\n    fill: var(\n      --xy-background-pattern-color-props,\n      var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default))\n    );\n  }\n.react-flow__background-pattern.lines {\n    stroke: var(\n      --xy-background-pattern-color-props,\n      var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default))\n    );\n  }\n.react-flow__background-pattern.cross {\n    stroke: var(\n      --xy-background-pattern-color-props,\n      var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default))\n    );\n  }\n.react-flow__controls {\n  display: flex;\n  flex-direction: column;\n}\n.react-flow__controls.horizontal {\n    flex-direction: row;\n  }\n.react-flow__controls-button {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    height: 26px;\n    width: 26px;\n    padding: 4px;\n  }\n.react-flow__controls-button svg {\n      width: 100%;\n      max-width: 12px;\n      max-height: 12px;\n      fill: currentColor;\n    }\n.react-flow__node-input,\n.react-flow__node-default,\n.react-flow__node-output,\n.react-flow__node-group {\n  border: var(--xy-node-border, var(--xy-node-border-default));\n  color: var(--xy-node-color, var(--xy-node-color-default));\n}\n.react-flow__node-input.selected,\n  .react-flow__node-input:focus,\n  .react-flow__node-input:focus-visible,\n  .react-flow__node-default.selected,\n  .react-flow__node-default:focus,\n  .react-flow__node-default:focus-visible,\n  .react-flow__node-output.selected,\n  .react-flow__node-output:focus,\n  .react-flow__node-output:focus-visible,\n  .react-flow__node-group.selected,\n  .react-flow__node-group:focus,\n  .react-flow__node-group:focus-visible {\n    outline: none;\n    border: var(--xy-node-border-selected, var(--xy-node-border-selected-default));\n  }\n.react-flow__nodesselection-rect,\n.react-flow__selection {\n  background: var(--xy-selection-background-color, var(--xy-selection-background-color-default));\n  border: var(--xy-selection-border, var(--xy-selection-border-default));\n}\n.react-flow__resize-control {\n  position: absolute;\n}\n.react-flow__resize-control.left,\n.react-flow__resize-control.right {\n  cursor: ew-resize;\n}\n.react-flow__resize-control.top,\n.react-flow__resize-control.bottom {\n  cursor: ns-resize;\n}\n.react-flow__resize-control.top.left,\n.react-flow__resize-control.bottom.right {\n  cursor: nwse-resize;\n}\n.react-flow__resize-control.bottom.left,\n.react-flow__resize-control.top.right {\n  cursor: nesw-resize;\n}\n/* handle styles */\n.react-flow__resize-control.handle {\n  width: 5px;\n  height: 5px;\n  border: 1px solid #fff;\n  border-radius: 1px;\n  background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));\n  translate: -50% -50%;\n}\n.react-flow__resize-control.handle.left {\n  left: 0;\n  top: 50%;\n}\n.react-flow__resize-control.handle.right {\n  left: 100%;\n  top: 50%;\n}\n.react-flow__resize-control.handle.top {\n  left: 50%;\n  top: 0;\n}\n.react-flow__resize-control.handle.bottom {\n  left: 50%;\n  top: 100%;\n}\n.react-flow__resize-control.handle.top.left {\n  left: 0;\n}\n.react-flow__resize-control.handle.bottom.left {\n  left: 0;\n}\n.react-flow__resize-control.handle.top.right {\n  left: 100%;\n}\n.react-flow__resize-control.handle.bottom.right {\n  left: 100%;\n}\n/* line styles */\n.react-flow__resize-control.line {\n  border-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));\n  border-width: 0;\n  border-style: solid;\n}\n.react-flow__resize-control.line.left,\n.react-flow__resize-control.line.right {\n  width: 1px;\n  transform: translate(-50%, 0);\n  top: 0;\n  height: 100%;\n}\n.react-flow__resize-control.line.left {\n  left: 0;\n  border-left-width: 1px;\n}\n.react-flow__resize-control.line.right {\n  left: 100%;\n  border-right-width: 1px;\n}\n.react-flow__resize-control.line.top,\n.react-flow__resize-control.line.bottom {\n  height: 1px;\n  transform: translate(0, -50%);\n  left: 0;\n  width: 100%;\n}\n.react-flow__resize-control.line.top {\n  top: 0;\n  border-top-width: 1px;\n}\n.react-flow__resize-control.line.bottom {\n  border-bottom-width: 1px;\n  top: 100%;\n}\n\n}\n",
      "type": "registry:file",
      "target": "styles/vlak/workflow.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "ai",
      "classes": [
        "rs-workflow-canvas",
        "rs-workflow-canvas-node",
        "rs-workflow-canvas-selected",
        "rs-workflow-canvas-header",
        "rs-workflow-canvas-title",
        "rs-workflow-canvas-description",
        "rs-workflow-canvas-content",
        "rs-workflow-canvas-footer",
        "rs-workflow-canvas-handle",
        "rs-workflow-canvas-dot",
        "rs-workflow-canvas-edge",
        "rs-workflow-canvas-temporary",
        "rs-workflow-canvas-animated",
        "rs-workflow-canvas-endpoint",
        "rs-workflow-canvas-panel",
        "rs-workflow-canvas-controls"
      ],
      "snippet": "<section class=\"rs-workflow-canvas\" aria-label=\"Workflow\"><div class=\"rs-workflow-canvas-node\"><header class=\"rs-workflow-canvas-header\"><div class=\"rs-workflow-canvas-title\">Review brief</div></header><div class=\"rs-workflow-canvas-content\">Summarize the supplied document.</div></div></section>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "canvas-controls",
        "toolbar",
        "vlak-lib"
      ],
      "reactImport": "@noorddev/vlak-react/components/workflow-canvas",
      "dependencies": [
        "@xyflow/react"
      ],
      "styles": [
        "@noorddev/vlak-react/workflow.css"
      ],
      "aliases": [
        "AI Elements Canvas",
        "AI Elements Connection",
        "AI Elements Controls",
        "AI Elements Edge",
        "AI Elements Node",
        "AI Elements Panel",
        "AI Elements Toolbar",
        "React Flow",
        "WorkflowCanvas",
        "WorkflowNode",
        "WorkflowEdge",
        "WorkflowConnection",
        "WorkflowControls",
        "WorkflowPanel",
        "WorkflowToolbar",
        "Interactive graph"
      ],
      "example": "\"use client\";\nimport \"@noorddev/vlak-react/workflow.css\";\nimport { addEdge, useNodesState, useEdgesState, type Edge } from \"@xyflow/react\";\nimport { WorkflowCanvas, WorkflowControls, WorkflowPanel, type WorkflowGraphNode } from \"@noorddev/vlak-react/components/workflow-canvas\";\n\nexport function ReviewWorkflow() {\n  const [nodes, , onNodesChange] = useNodesState<WorkflowGraphNode>([\n    { id: \"brief\", type: \"vlak\", position: { x: 0, y: 0 }, data: { title: \"Read brief\", target: false } },\n    { id: \"review\", type: \"vlak\", position: { x: 380, y: 0 }, data: { title: \"Review\", source: false } },\n  ]);\n  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);\n  return <WorkflowCanvas nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}\n    onConnect={connection => setEdges(current => addEdge({ ...connection, type: \"vlak\" }, current))}>\n    <WorkflowControls />\n    <WorkflowPanel position=\"top-left\">Review workflow</WorkflowPanel>\n  </WorkflowCanvas>;\n}",
      "usage": {
        "use": [
          "Import this optional entry and its stylesheet only where a graph is needed. React Flow is an optional peer dependency.",
          "Supply nodes, edges, and change callbacks for a controlled graph, or defaultNodes and defaultEdges for engine-owned state. The application persists and executes the graph.",
          "Use type=vlak for the supplied node and edge renderers. Custom nodeTypes can compose WorkflowNode and WorkflowToolbar; custom connection lines can use WorkflowConnection.",
          "WorkflowPanel anchors overlays in six viewport positions. WorkflowToolbar stays anchored to a selected node and composes Vlak's keyboard-aware Toolbar.",
          "WorkflowControls binds the existing CanvasControls to pan, zoom, fit, reset, and interaction lock. React Flow props remain available, including connection validation, viewport changes, and accessibility configuration.",
          "For copied source, import @xyflow/react/dist/base.css in a CSS layer named vlak.engine before vlak.components. The package's workflow.css supplies that ordering.",
          "Set an explicit height through style when the default 32rem does not suit the container. Use a separate form or Patchbay when users need keyboard-only creation of connections."
        ],
        "avoid": [
          "Using Flow, an ordered pipeline layout, as an interactive graph engine.",
          "Turning off React Flow's keyboard accessibility without supplying equivalent controls.",
          "Treating a connected graph as executed work. Execution, persistence, permissions, and undo history belong to the application."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses nodes, edges, viewport controls, and supplied actions"
        },
        {
          "keys": "Enter, Space",
          "does": "Selects a focused node or edge through React Flow"
        },
        {
          "keys": "Arrow keys",
          "does": "Moves a selected, draggable node; moves between actions inside the node toolbar"
        },
        {
          "keys": "Delete, Backspace",
          "does": "Deletes selected graph elements through the supplied change callbacks"
        },
        {
          "keys": "Escape",
          "does": "Cancels the current graph selection or connection gesture"
        }
      ],
      "a11y": [
        "React Flow supplies keyboard movement, focusable nodes and edges, instruction text, and movement announcements. ariaLabelConfig can localize those messages.",
        "Visible connection handles have 44px pointer targets. The application should provide a form-based connection action for keyboard-only creation.",
        "The node action Toolbar retains its single Tab stop and arrow-key focus movement.",
        "Animated edges stop moving with reduced motion. Surfaces use Vlak paper, ink, hairlines, and 4px corners."
      ]
    }
  }
}
