n.type === 'error' ? 'red' : '#ddd'}
+ width={200}
+ height={150}
+/>
+```
+
+### Controls
+
+Zoom in/out, fit view with SVG icons and zoom percentage.
+
+```tsx
+
+```
+
+### NodeResizer
+
+Drag handles on corners (and optionally edges) to resize nodes. Uses pointer capture — no event leaks.
+
+```tsx
+function ResizableNode({ id, data, selected }: NodeComponentProps) {
+ return (
+
+ {data.label}
+
+
+ )
+}
+```
+
+### NodeToolbar
+
+Floating toolbar that appears when a node is selected.
+
+```tsx
+function EditableNode({ id, data, selected }: NodeComponentProps) {
+ return (
+
+ {data.label}
+
+
+
+
+
+ )
+}
+```
+
+### Panel
+
+Positioned overlay for custom content.
+
+```tsx
+
+ {
+ const results = flow.searchNodes(e.target.value)
+ if (results[0]) flow.focusNode(results[0].id)
+ }} />
+
+```
+
+### Handle
+
+Connection point on nodes.
+
+```tsx
+
+
+```
+
+## Keyboard Shortcuts
+
+Built-in when the Flow component is focused:
+
+| Shortcut | Action |
+|---|---|
+| `Delete` / `Backspace` | Delete selected nodes/edges |
+| `Escape` | Clear selection |
+| `Cmd/Ctrl + A` | Select all |
+| `Cmd/Ctrl + C` | Copy selected |
+| `Cmd/Ctrl + V` | Paste |
+| `Cmd/Ctrl + Z` | Undo |
+| `Cmd/Ctrl + Shift + Z` | Redo |
+
+## Copy / Paste
+
+```tsx
+flow.copySelected() // copies selected nodes + internal edges
+flow.paste() // paste with offset, new IDs
+flow.paste({ x: 100, y: 50 }) // custom offset
+```
+
+## Undo / Redo
+
+```tsx
+flow.pushHistory() // save current state
+flow.undo() // restore previous
+flow.redo() // restore next
+
+// History is auto-saved before drag and delete operations
+```
+
+## Advanced Features
+
+### Helper Lines (Snap Guides)
+
+Automatic alignment guides appear when dragging nodes near other nodes' edges or centers.
+
+### Multi-Node Drag
+
+Select multiple nodes (shift+click or rubber band), then drag — all move together with snap guides on the primary node.
+
+### Touch Support
+
+Pinch-to-zoom and two-finger pan on touch devices.
+
+### Collision Detection
+
+```tsx
+flow.getOverlappingNodes('1') // nodes that overlap with node '1'
+flow.resolveCollisions('1') // push overlapping nodes apart
+```
+
+### Proximity Connect
+
+```tsx
+const connection = flow.getProximityConnection('1', 50)
+// Returns Connection | null — nearest unconnected node within threshold
+```
+
+### Node Extent (Drag Boundaries)
+
+```tsx
+flow.setNodeExtent([[0, 0], [1000, 800]]) // constrain all nodes
+flow.setNodeExtent(null) // remove constraint
+```
+
+### Sub-Flows / Groups
+
+```tsx
+flow.addNode({ id: 'group', position: { x: 0, y: 0 }, group: true, data: { label: 'API Layer' } })
+flow.addNode({ id: 'child', position: { x: 10, y: 10 }, parentId: 'group', data: { label: 'GET /users' } })
+
+flow.getChildNodes('group') // child nodes
+flow.getAbsolutePosition('child') // { x: 10, y: 10 } + parent offset
+```
+
+### Graph Queries
+
+```tsx
+flow.getConnectedEdges('1') // all edges touching node '1'
+flow.getIncomers('3') // nodes with edges pointing to '3'
+flow.getOutgoers('1') // nodes that '1' points to
+```
+
+### Export / Import
+
+```tsx
+const json = flow.toJSON() // { nodes, edges, viewport }
+flow.fromJSON(json) // restore state
+
+// Save to localStorage
+localStorage.setItem('my-flow', JSON.stringify(flow.toJSON()))
+```
+
+### CSS Styles
+
+```tsx
+import { flowStyles } from '@pyreon/flow'
+
+// Inject once — provides animated edges, hover states, transitions
+const style = document.createElement('style')
+style.textContent = flowStyles
+document.head.appendChild(style)
+```
+
+## Event Callbacks
+
+```tsx
+flow.onConnect((connection) => console.log('Connected:', connection))
+flow.onNodesChange((changes) => console.log('Changed:', changes))
+flow.onNodeClick((node) => console.log('Clicked:', node.id))
+flow.onEdgeClick((edge) => console.log('Edge clicked:', edge.id))
+flow.onNodeDragStart((node) => console.log('Drag start:', node.id))
+flow.onNodeDragEnd((node) => console.log('Drag end:', node.id))
+flow.onNodeDoubleClick((node) => console.log('Double-click:', node.id))
+
+// All return unsubscribe functions
+const unsub = flow.onConnect(handler)
+unsub() // remove listener
+```
+
+## Edge Path Utilities
+
+For custom edge rendering:
+
+```tsx
+import { getBezierPath, getSmoothStepPath, getStraightPath, getStepPath, getWaypointPath } from '@pyreon/flow'
+
+const { path, labelX, labelY } = getBezierPath({
+ sourceX: 0, sourceY: 0, sourcePosition: Position.Right,
+ targetX: 200, targetY: 100, targetPosition: Position.Left,
+})
+```
+
+## API Reference
+
+| API | Description |
+|---|---|
+| `createFlow(config)` | Create flow instance |
+| `flow.nodes` / `edges` / `viewport` | Reactive signals |
+| `flow.addNode()` / `removeNode()` / `updateNode()` | Node CRUD |
+| `flow.addEdge()` / `removeEdge()` / `reconnectEdge()` | Edge CRUD |
+| `flow.selectNode()` / `selectAll()` / `clearSelection()` / `deleteSelected()` | Selection |
+| `flow.zoomIn()` / `zoomOut()` / `fitView()` / `panTo()` / `focusNode()` | Viewport |
+| `flow.layout(algorithm, options)` | Auto-layout (elkjs) |
+| `flow.copySelected()` / `paste()` | Clipboard |
+| `flow.pushHistory()` / `undo()` / `redo()` | History |
+| `flow.searchNodes()` / `findNodes()` | Search |
+| `flow.toJSON()` / `fromJSON()` | Serialize |
+| `flow.getSnapLines()` | Helper lines |
+| `flow.getOverlappingNodes()` / `resolveCollisions()` | Collision |
+| `flow.getProximityConnection()` | Proximity connect |
+| `flow.setNodeExtent()` | Drag boundaries |
+| `flow.getChildNodes()` / `getAbsolutePosition()` | Sub-flows |
+| `flow.animateViewport()` | Smooth transitions |
+| `flow.batch()` | Batch operations |
+| `flow.dispose()` | Cleanup |
+
+## Components
+
+| Component | Description |
+|---|---|
+| `` | Main container — renders nodes, edges, handles interactions |
+| `` | Dot/line/cross pattern |
+| `` | Overview with viewport rect + click navigation |
+| `` | Zoom buttons with percentage |
+| `` | Connection point on nodes |
+| `` | Positioned overlay |
+| `` | Drag-to-resize handles |
+| `` | Floating toolbar on select |