@infinit-canvas/react 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,61 +1,1098 @@
1
- # react-infinite-canvas
1
+ # @infinit-canvas/react
2
2
 
3
- A high-performance infinite canvas React component powered by OffscreenCanvas and Web Workers. Handles 2000+ cards at 60fps with spatial indexing, batched rendering, and automatic LOD.
3
+ A high-performance infinite canvas for React. Pan, zoom, drag nodes, draw edges all rendered on a background Web Worker so your UI never freezes, even with 5000+ nodes.
4
+
5
+ **Drop-in compatible with React Flow API** — same props, hooks, and patterns. If you know React Flow, you already know this.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Key Concepts (Glossary)](#key-concepts-glossary)
12
+ - [Install](#install)
13
+ - [Quick Start](#quick-start)
14
+ - [Nodes](#nodes)
15
+ - [Edges](#edges)
16
+ - [Handles](#handles)
17
+ - [Custom Nodes](#custom-nodes)
18
+ - [Custom Edges](#custom-edges)
19
+ - [State Management](#state-management)
20
+ - [Hooks](#hooks)
21
+ - [Viewport & Camera](#viewport--camera)
22
+ - [Interactions](#interactions)
23
+ - [Styling & Appearance](#styling--appearance)
24
+ - [Components](#components)
25
+ - [Utilities](#utilities)
26
+ - [Performance](#performance)
27
+ - [Full Props Reference](#full-props-reference)
28
+
29
+ ---
30
+
31
+ ## Key Concepts (Glossary)
32
+
33
+ If you're new to canvas/graph libraries, here's what each term means:
34
+
35
+ ### Canvas
36
+
37
+ The drawing surface. Think of it as an infinite whiteboard you can pan around and zoom into. Under the hood, this uses an HTML `<canvas>` element transferred to a **Web Worker** so all drawing happens off the main thread.
38
+
39
+ ### Node
40
+
41
+ A box/card on the canvas. Each node has a position (x, y), dimensions (width, height), and data (any info you want to store). Nodes are the "things" on your canvas — could be workflow steps, diagram boxes, sticky notes, etc.
42
+
43
+ ### Edge
44
+
45
+ A line connecting two nodes. An edge goes **from** a source node **to** a target node. Edges can be straight lines, curves (bezier), or right-angle paths (smoothstep/step).
46
+
47
+ ### Handle
48
+
49
+ A connection point on a node. By default every node has two handles: a **source** handle (right side, where edges come out) and a **target** handle (left side, where edges go in). You can customize handle positions and add multiple handles.
50
+
51
+ ### Viewport / Camera
52
+
53
+ The visible area of the canvas. When you pan or zoom, you're moving the camera. The viewport is defined by three values: `x` (horizontal offset), `y` (vertical offset), and `zoom` (scale level).
54
+
55
+ ### World Space vs Screen Space
56
+
57
+ - **World space**: The coordinate system of the canvas. A node at position `{x: 500, y: 300}` is always at that world position, no matter where you've panned or zoomed.
58
+ - **Screen space**: Pixel coordinates on your monitor. Where something appears on screen depends on the camera position and zoom level.
59
+
60
+ ### Panning
61
+
62
+ Click-and-drag on empty canvas space to move around. The camera position changes, but nodes stay at their world positions.
63
+
64
+ ### Zooming
65
+
66
+ Scroll wheel, pinch gesture, or double-click to zoom in/out. Zooming scales everything relative to the cursor position.
67
+
68
+ ### Frustum Culling
69
+
70
+ A performance technique: only draw what's visible on screen. If you have 5000 nodes but only 50 are visible in the viewport, only those 50 get rendered. The canvas uses a **spatial grid** to find visible nodes in O(1) time instead of checking all 5000.
71
+
72
+ ### Web Worker / OffscreenCanvas
73
+
74
+ JavaScript normally runs on a single thread — the "main thread." Heavy canvas drawing on the main thread blocks React updates and user input. This library moves ALL canvas rendering to a **Web Worker** (a separate thread) using **OffscreenCanvas**, so your UI stays responsive.
75
+
76
+ ### Level of Detail (LOD)
77
+
78
+ When zoomed far out, small details like text labels and shadows are invisible anyway. LOD skips rendering these at low zoom levels to save drawing time.
79
+
80
+ ### Spatial Grid / Spatial Index
81
+
82
+ A data structure that divides the world into grid cells. Each cell knows which nodes are inside it. To find visible nodes, you only check cells that overlap the viewport — much faster than checking every node individually.
83
+
84
+ ### Edge Routing
85
+
86
+ Smart edge paths that go around obstacles. Instead of drawing a straight line through other nodes, the edge routing system computes right-angle paths that avoid overlapping with nodes.
87
+
88
+ ### Batching
89
+
90
+ Instead of drawing each node/edge one at a time (thousands of draw calls), the renderer groups them and draws all at once (3-10 draw calls total). Fewer draw calls = faster rendering.
91
+
92
+ ### Zustand Store
93
+
94
+ A lightweight state manager used internally. When you wrap your app in `<InfiniteCanvasProvider>`, it creates a Zustand store that hooks like `useNodes()`, `useStore()`, and `useReactFlow()` subscribe to.
95
+
96
+ ---
4
97
 
5
98
  ## Install
6
99
 
7
100
  ```bash
8
- npm install react-infinite-canvas
101
+ npm install @infinit-canvas/react
9
102
  ```
10
103
 
11
- ## Usage
104
+ ---
105
+
106
+ ## Quick Start
107
+
108
+ ### Simplest example — just nodes and edges
12
109
 
13
110
  ```jsx
14
- import { InfiniteCanvas } from 'react-infinite-canvas';
15
- import 'react-infinite-canvas/styles.css';
111
+ import { useState, useCallback } from 'react';
112
+ import {
113
+ InfiniteCanvas,
114
+ applyNodeChanges,
115
+ applyEdgeChanges,
116
+ addEdge,
117
+ } from '@infinit-canvas/react';
118
+ import '@infinit-canvas/react/styles.css';
119
+
120
+ // Define your nodes — each needs a unique id and a position
121
+ const initialNodes = [
122
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Start' } },
123
+ { id: '2', position: { x: 200, y: 100 }, data: { label: 'End' } },
124
+ ];
16
125
 
17
- const cards = [
18
- { x: 60, y: 80, w: 160, h: 90, title: 'Note A', body: 'Hello!' },
19
- { x: 320, y: 140, w: 160, h: 90, title: 'Note B', body: 'World!' },
126
+ // Define your edges — each connects a source node to a target node
127
+ const initialEdges = [
128
+ { id: 'e1-2', source: '1', target: '2' },
20
129
  ];
21
130
 
22
131
  function App() {
23
- return <InfiniteCanvas cards={cards} height="500px" />;
132
+ const [nodes, setNodes] = useState(initialNodes);
133
+ const [edges, setEdges] = useState(initialEdges);
134
+
135
+ // Called when nodes are dragged, selected, or removed
136
+ const onNodesChange = useCallback(
137
+ (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
138
+ []
139
+ );
140
+
141
+ // Called when edges are selected or removed
142
+ const onEdgesChange = useCallback(
143
+ (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
144
+ []
145
+ );
146
+
147
+ // Called when user drags from one handle to another
148
+ const onConnect = useCallback(
149
+ (connection) => setEdges((eds) => addEdge(connection, eds)),
150
+ []
151
+ );
152
+
153
+ return (
154
+ <InfiniteCanvas
155
+ nodes={nodes}
156
+ edges={edges}
157
+ onNodesChange={onNodesChange}
158
+ onEdgesChange={onEdgesChange}
159
+ onConnect={onConnect}
160
+ fitView // auto-zoom to fit all nodes on mount
161
+ height="100vh"
162
+ />
163
+ );
24
164
  }
25
165
  ```
26
166
 
27
- ## Props
167
+ ### Even simpler — with helper hooks
28
168
 
29
- | Prop | Default | Description |
30
- |------|---------|-------------|
31
- | `cards` | `[]` | Array of `{ x, y, w, h, title, body }` |
32
- | `dark` | auto | Force dark/light mode |
33
- | `gridSize` | `40` | Grid cell size in px |
34
- | `zoomMin` / `zoomMax` | `0.1` / `4` | Zoom limits |
35
- | `initialCamera` | `{ x:0, y:0, zoom:1 }` | Starting position |
36
- | `onHudUpdate` | - | Callback: `{ wx, wy, zoom, renderMs, fps, visible }` |
37
- | `showHud` / `showHint` | `true` | Toggle overlays |
38
- | `width` / `height` | `'100%'` / `'420px'` | Container size |
39
- | `className` / `style` | - | Custom styling |
40
- | `children` | - | Render inside canvas wrapper |
169
+ ```jsx
170
+ import {
171
+ InfiniteCanvas,
172
+ useNodesState,
173
+ useEdgesState,
174
+ addEdge,
175
+ } from '@infinit-canvas/react';
176
+ import '@infinit-canvas/react/styles.css';
41
177
 
42
- ## Hook API
178
+ const initialNodes = [
179
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Hello' } },
180
+ { id: '2', position: { x: 250, y: 100 }, data: { label: 'World' } },
181
+ ];
182
+
183
+ const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
184
+
185
+ function App() {
186
+ // useNodesState/useEdgesState handle the onChange boilerplate for you
187
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
188
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
189
+
190
+ return (
191
+ <InfiniteCanvas
192
+ nodes={nodes}
193
+ edges={edges}
194
+ onNodesChange={onNodesChange}
195
+ onEdgesChange={onEdgesChange}
196
+ onConnect={(conn) => setEdges((eds) => addEdge(conn, eds))}
197
+ fitView
198
+ height="100vh"
199
+ />
200
+ );
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Nodes
207
+
208
+ A node represents an item on the canvas.
209
+
210
+ ```js
211
+ {
212
+ id: 'node-1', // Required. Unique identifier.
213
+ position: { x: 100, y: 50 }, // Required. World-space coordinates.
214
+ data: { label: 'My Node' }, // Required. Your custom data — passed to custom node components.
215
+ type: 'default', // Optional. Which node component to render (see Custom Nodes).
216
+ width: 160, // Optional. Width in pixels (default: 160).
217
+ height: 60, // Optional. Height in pixels (default: 60).
218
+ selected: false, // Optional. Is this node selected?
219
+ hidden: false, // Optional. Hide this node entirely.
220
+ draggable: true, // Optional. Can the user drag this node?
221
+ parentId: 'group-1', // Optional. Makes this a child of another node (sub-flows).
222
+ handles: [ // Optional. Custom connection handles.
223
+ { type: 'source', position: 'bottom' },
224
+ { type: 'target', position: 'top' },
225
+ ],
226
+ style: { background: 'red' }, // Optional. CSS styles (only for custom-rendered nodes).
227
+ }
228
+ ```
229
+
230
+ ### Node Types
231
+
232
+ - **No type / `'default'`** — Rendered by the canvas worker (fast, drawn as a rounded rectangle with label)
233
+ - **`'input'`** — Built-in node with only a source handle (output only)
234
+ - **`'output'`** — Built-in node with only a target handle (input only)
235
+ - **`'group'`** — Container node that can hold child nodes (set `parentId` on children)
236
+ - **Custom type** — Your own React component (see [Custom Nodes](#custom-nodes))
237
+
238
+ ---
239
+
240
+ ## Edges
241
+
242
+ An edge is a visual connection between two nodes.
243
+
244
+ ```js
245
+ {
246
+ id: 'edge-1', // Required. Unique identifier.
247
+ source: 'node-1', // Required. ID of the source node (where the edge starts).
248
+ target: 'node-2', // Required. ID of the target node (where the edge ends).
249
+ type: 'default', // Optional. Edge style: 'default' | 'straight' | 'smoothstep' | 'step' | 'bezier'
250
+ label: 'connects', // Optional. Text label shown at the edge midpoint.
251
+ animated: true, // Optional. Dashed line animation (marching ants).
252
+ selected: false, // Optional. Is this edge selected?
253
+ sourceHandle: 'a', // Optional. Which handle on the source node (if it has multiple).
254
+ targetHandle: 'b', // Optional. Which handle on the target node.
255
+ style: {}, // Optional. CSS styles (only for custom-rendered edges).
256
+ }
257
+ ```
258
+
259
+ ### Edge Types
260
+
261
+ | Type | Description |
262
+ |------|-------------|
263
+ | `'default'` / `'bezier'` | Smooth curved line (cubic bezier). Good for most use cases. |
264
+ | `'straight'` | Direct straight line between handles. |
265
+ | `'smoothstep'` | Right-angle path with rounded corners. Great for flowcharts. Supports edge routing (obstacle avoidance). |
266
+ | `'step'` | Right-angle path with sharp corners. Like smoothstep but without the rounded bends. |
267
+
268
+ ---
269
+
270
+ ## Handles
271
+
272
+ Handles are the connection points on nodes — the dots you drag from/to to create edges.
273
+
274
+ ```jsx
275
+ import { Handle, Position } from '@infinit-canvas/react';
276
+
277
+ function MyNode({ data }) {
278
+ return (
279
+ <div style={{ padding: 10, border: '1px solid #ccc' }}>
280
+ {/* Target handle on top — edges can connect TO this */}
281
+ <Handle type="target" position={Position.Top} />
282
+
283
+ <span>{data.label}</span>
284
+
285
+ {/* Source handle on bottom — edges can start FROM this */}
286
+ <Handle type="source" position={Position.Bottom} />
287
+ </div>
288
+ );
289
+ }
290
+ ```
291
+
292
+ ### Handle positions
293
+
294
+ - `Position.Top` — Top center of the node
295
+ - `Position.Bottom` — Bottom center
296
+ - `Position.Left` — Left center
297
+ - `Position.Right` — Right center (default for source handles)
298
+
299
+ ### Multiple handles
300
+
301
+ Add an `id` to distinguish them:
43
302
 
44
303
  ```jsx
45
- import { useInfiniteCanvas } from 'react-infinite-canvas';
304
+ <Handle type="source" position={Position.Right} id="output-a" />
305
+ <Handle type="source" position={Position.Bottom} id="output-b" />
306
+ ```
307
+
308
+ Then in edges: `{ source: 'node-1', sourceHandle: 'output-a', target: 'node-2' }`
309
+
310
+ ---
311
+
312
+ ## Custom Nodes
313
+
314
+ By default, nodes are drawn by the canvas worker (fast rectangles with text). For rich UI (buttons, inputs, images), register a custom React component:
315
+
316
+ ```jsx
317
+ // 1. Define your component — receives node props
318
+ function ColorPickerNode({ data, selected }) {
319
+ return (
320
+ <div style={{
321
+ padding: 10,
322
+ background: data.color,
323
+ border: selected ? '2px solid blue' : '1px solid #ccc',
324
+ borderRadius: 8,
325
+ }}>
326
+ <Handle type="target" position={Position.Left} />
327
+ <span>{data.label}</span>
328
+ <input
329
+ type="color"
330
+ value={data.color}
331
+ onChange={(e) => data.onChange?.(e.target.value)}
332
+ style={{ pointerEvents: 'all' }} // Important! Nodes overlay has pointerEvents: none
333
+ />
334
+ <Handle type="source" position={Position.Right} />
335
+ </div>
336
+ );
337
+ }
338
+
339
+ // 2. Register it in nodeTypes (must be defined OUTSIDE the component, or memoized)
340
+ const nodeTypes = { colorPicker: ColorPickerNode };
341
+
342
+ // 3. Use it
343
+ const nodes = [
344
+ {
345
+ id: '1',
346
+ type: 'colorPicker', // matches the key in nodeTypes
347
+ position: { x: 0, y: 0 },
348
+ data: { label: 'Pick a color', color: '#ff6600' },
349
+ },
350
+ ];
46
351
 
47
- const { wrapRef, canvasRef, onPointerDown, onPointerMove, onPointerUp,
48
- resetView, addCard, getCamera, setCamera } = useInfiniteCanvas({ cards });
352
+ function App() {
353
+ return (
354
+ <InfiniteCanvas nodes={nodes} nodeTypes={nodeTypes} ... />
355
+ );
356
+ }
357
+ ```
358
+
359
+ **Important**: Nodes without a `type` (or with `type: 'default'`) are rendered by the canvas worker, NOT as React components. Only nodes with a `type` that matches a key in `nodeTypes` become React components.
360
+
361
+ ---
362
+
363
+ ## Custom Edges
364
+
365
+ Same pattern as custom nodes:
366
+
367
+ ```jsx
368
+ import { EdgeLabelRenderer } from '@infinit-canvas/react';
369
+
370
+ function CustomEdge({ id, sourceX, sourceY, targetX, targetY, label, style }) {
371
+ // Draw your own SVG path
372
+ const path = `M ${sourceX} ${sourceY} L ${targetX} ${targetY}`;
373
+
374
+ return (
375
+ <>
376
+ <path d={path} stroke="#ff0000" strokeWidth={2} fill="none" style={style} />
377
+ {label && (
378
+ <EdgeLabelRenderer>
379
+ <div style={{
380
+ position: 'absolute',
381
+ transform: `translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`,
382
+ pointerEvents: 'all',
383
+ }}>
384
+ {label}
385
+ </div>
386
+ </EdgeLabelRenderer>
387
+ )}
388
+ </>
389
+ );
390
+ }
391
+
392
+ const edgeTypes = { custom: CustomEdge };
393
+ // Then use: { id: 'e1', source: '1', target: '2', type: 'custom' }
394
+ ```
395
+
396
+ ---
397
+
398
+ ## State Management
399
+
400
+ ### How data flows
401
+
402
+ ```
403
+ Your component (owns nodes/edges state)
404
+
405
+ ├── passes nodes, edges as props ──→ <InfiniteCanvas>
406
+
407
+ ├── onNodesChange callback ◄──────── user drags/selects/deletes a node
408
+ │ └── you call applyNodeChanges() to update your state
409
+
410
+ ├── onEdgesChange callback ◄──────── user selects/deletes an edge
411
+ │ └── you call applyEdgeChanges() to update your state
412
+
413
+ └── onConnect callback ◄──────────── user drags handle-to-handle
414
+ └── you call addEdge() to add the new edge
415
+ ```
416
+
417
+ **You own the data.** The canvas tells you what changed via callbacks, and you decide whether to apply those changes. This is the "controlled component" pattern — same as a React `<input>` with `value` and `onChange`.
418
+
419
+ ### useNodesState / useEdgesState
420
+
421
+ Convenience hooks that set up `useState` + the `onChange` handler for you:
422
+
423
+ ```jsx
424
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
425
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
426
+ ```
427
+
428
+ Equivalent to manually writing:
429
+
430
+ ```jsx
431
+ const [nodes, setNodes] = useState(initialNodes);
432
+ const onNodesChange = useCallback(
433
+ (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
434
+ []
435
+ );
436
+ ```
437
+
438
+ ### InfiniteCanvasProvider
439
+
440
+ Wrap your tree in `<InfiniteCanvasProvider>` when you need hooks ABOVE the `<InfiniteCanvas>` component (e.g., for auto-layout, or accessing `useReactFlow()` from a sibling component):
441
+
442
+ ```jsx
443
+ import { InfiniteCanvasProvider, InfiniteCanvas, useReactFlow } from '@infinit-canvas/react';
444
+
445
+ function Toolbar() {
446
+ // This works because InfiniteCanvasProvider is above us
447
+ const { fitView, zoomIn, zoomOut } = useReactFlow();
448
+ return (
449
+ <div>
450
+ <button onClick={() => zoomIn()}>+</button>
451
+ <button onClick={() => zoomOut()}>-</button>
452
+ <button onClick={() => fitView()}>Fit</button>
453
+ </div>
454
+ );
455
+ }
456
+
457
+ function App() {
458
+ return (
459
+ <InfiniteCanvasProvider>
460
+ <Toolbar />
461
+ <InfiniteCanvas nodes={nodes} edges={edges} ... />
462
+ </InfiniteCanvasProvider>
463
+ );
464
+ }
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Hooks
470
+
471
+ ### useReactFlow()
472
+
473
+ Imperative API to control the canvas programmatically. Does NOT cause re-renders — uses refs internally.
474
+
475
+ ```jsx
476
+ const {
477
+ // Read
478
+ getNodes, // () => Node[]
479
+ getEdges, // () => Edge[]
480
+ getNode, // (id) => Node | undefined
481
+ getEdge, // (id) => Edge | undefined
482
+ getViewport, // () => { x, y, zoom }
483
+ getZoom, // () => number
484
+
485
+ // Write
486
+ setNodes, // (nodes | updaterFn) => void
487
+ setEdges, // (edges | updaterFn) => void
488
+ addNodes, // (node | nodes) => void
489
+ addEdges, // (edge | edges) => void
490
+ deleteElements, // ({ nodes?, edges? }) => void
491
+ updateNodeData, // (nodeId, dataUpdate) => void
492
+
493
+ // Camera
494
+ setViewport, // ({ x?, y?, zoom? }, { duration? }) => void
495
+ zoomIn, // ({ duration? }) => void
496
+ zoomOut, // ({ duration? }) => void
497
+ zoomTo, // (zoom, { duration? }) => void
498
+ fitView, // ({ padding?, duration?, nodes? }) => void
499
+ fitBounds, // (bounds, { padding?, duration? }) => void
500
+ setCenter, // (x, y, { zoom?, duration? }) => void
501
+
502
+ // Coordinate conversion
503
+ screenToFlowPosition, // ({ x, y }) => { x, y } (screen pixels → world coords)
504
+ flowToScreenPosition, // ({ x, y }) => { x, y } (world coords → screen pixels)
505
+
506
+ // Serialize
507
+ toObject, // () => { nodes, edges, viewport }
508
+ } = useReactFlow();
509
+ ```
510
+
511
+ ### useNodes() / useEdges()
512
+
513
+ Subscribe to nodes/edges — re-renders when they change.
514
+
515
+ ```jsx
516
+ const nodes = useNodes();
517
+ const edges = useEdges();
518
+ ```
519
+
520
+ ### useStore(selector, equalityFn?)
521
+
522
+ Subscribe to specific slices of the internal store. Use a **selector** to pick only what you need (avoids unnecessary re-renders):
523
+
524
+ ```jsx
525
+ // Only re-renders when the node count changes
526
+ const nodeCount = useStore((state) => state.nodes.length);
527
+
528
+ // With custom equality function
529
+ const nodeIds = useStore(
530
+ (state) => state.nodes.map(n => n.id),
531
+ (a, b) => a.length === b.length && a.every((id, i) => id === b[i])
532
+ );
533
+ ```
534
+
535
+ ### useViewport()
536
+
537
+ Subscribe to viewport changes. Re-renders on every camera update.
538
+
539
+ ```jsx
540
+ const { x, y, zoom } = useViewport();
541
+ ```
542
+
543
+ ### useOnViewportChange({ onChange, onStart, onEnd })
544
+
545
+ Listen to viewport changes without re-rendering your component:
546
+
547
+ ```jsx
548
+ useOnViewportChange({
549
+ onChange: (viewport) => console.log('Moving:', viewport),
550
+ onStart: (viewport) => console.log('Pan/zoom started'),
551
+ onEnd: (viewport) => console.log('Pan/zoom ended'),
552
+ });
553
+ ```
554
+
555
+ ### useOnSelectionChange({ onChange })
556
+
557
+ ```jsx
558
+ useOnSelectionChange({
559
+ onChange: ({ nodes, edges }) => {
560
+ console.log('Selected nodes:', nodes);
561
+ console.log('Selected edges:', edges);
562
+ },
563
+ });
564
+ ```
565
+
566
+ ### useNodesData(nodeIds)
567
+
568
+ Get just the `data` for specific nodes:
569
+
570
+ ```jsx
571
+ const data = useNodesData(['node-1', 'node-2']);
572
+ // [{ id: 'node-1', type: 'default', data: { label: '...' } }, ...]
573
+ ```
574
+
575
+ ### useNodeConnections(nodeId)
576
+
577
+ Get all edges connected to a node:
578
+
579
+ ```jsx
580
+ const connections = useNodeConnections('node-1');
581
+ // Edge[]
582
+ ```
583
+
584
+ ### useHandleConnections({ nodeId, type, handleId? })
585
+
586
+ Get edges connected to a specific handle:
587
+
588
+ ```jsx
589
+ const incoming = useHandleConnections({ nodeId: 'node-1', type: 'target' });
590
+ ```
591
+
592
+ ### useKeyPress(key)
593
+
594
+ ```jsx
595
+ const shiftPressed = useKeyPress('Shift');
49
596
  ```
50
597
 
598
+ ### useUndoRedo({ maxHistorySize? })
599
+
600
+ ```jsx
601
+ const { undo, redo, takeSnapshot, canUndo, canRedo } = useUndoRedo();
602
+
603
+ // Call takeSnapshot() before making changes
604
+ function onNodeDragStop() {
605
+ takeSnapshot();
606
+ }
607
+ ```
608
+
609
+ ### useNodeId()
610
+
611
+ Inside a custom node component, get the current node's ID:
612
+
613
+ ```jsx
614
+ function MyNode({ data }) {
615
+ const nodeId = useNodeId();
616
+ return <div>I am {nodeId}</div>;
617
+ }
618
+ ```
619
+
620
+ ---
621
+
622
+ ## Viewport & Camera
623
+
624
+ ### Initial camera position
625
+
626
+ ```jsx
627
+ <InfiniteCanvas initialCamera={{ x: 100, y: 50, zoom: 1.5 }} />
628
+ ```
629
+
630
+ ### Fit all nodes on mount
631
+
632
+ ```jsx
633
+ <InfiniteCanvas fitView />
634
+ <InfiniteCanvas fitView fitViewOptions={{ padding: 0.2, maxZoom: 2 }} />
635
+ ```
636
+
637
+ ### Zoom limits
638
+
639
+ ```jsx
640
+ <InfiniteCanvas zoomMin={0.1} zoomMax={5} />
641
+ ```
642
+
643
+ ### Lock the camera to an area
644
+
645
+ ```jsx
646
+ // User can't pan beyond these world coordinates
647
+ <InfiniteCanvas translateExtent={[[-1000, -1000], [1000, 1000]]} />
648
+ ```
649
+
650
+ ### Programmatic camera control
651
+
652
+ ```jsx
653
+ const { setViewport, fitView, zoomTo, setCenter } = useReactFlow();
654
+
655
+ // Animated zoom to 2x over 500ms
656
+ zoomTo(2, { duration: 500 });
657
+
658
+ // Center on a specific world coordinate
659
+ setCenter(500, 300, { zoom: 1.5, duration: 300 });
660
+
661
+ // Fit all nodes with padding
662
+ fitView({ padding: 0.2, duration: 400 });
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Interactions
668
+
669
+ ### Panning
670
+
671
+ | Method | Default |
672
+ |--------|---------|
673
+ | Click + drag on empty space | Always enabled |
674
+ | Scroll wheel | Zooms (set `panOnScroll` to pan instead) |
675
+ | Space + drag | Pan with spacebar held (`panActivationKeyCode`) |
676
+
677
+ ```jsx
678
+ <InfiniteCanvas
679
+ panOnScroll // scroll wheel pans instead of zooming
680
+ panOnScrollMode="horizontal" // 'free' | 'horizontal' | 'vertical'
681
+ panOnScrollSpeed={0.5}
682
+ panActivationKeyCode=" " // spacebar activates pan mode
683
+ />
684
+ ```
685
+
686
+ ### Zooming
687
+
688
+ ```jsx
689
+ <InfiniteCanvas
690
+ zoomOnScroll // default: true
691
+ zoomOnPinch // default: true (two-finger pinch on trackpad/mobile)
692
+ zoomOnDoubleClick // default: true
693
+ zoomMin={0.1}
694
+ zoomMax={5}
695
+ />
696
+ ```
697
+
698
+ ### Node dragging
699
+
700
+ ```jsx
701
+ <InfiniteCanvas
702
+ nodesDraggable // default: true — all nodes can be dragged
703
+ snapToGrid // snap to grid while dragging
704
+ snapGrid={[25, 25]} // grid spacing [x, y] in pixels
705
+ nodeExtent={[[-500, -500], [500, 500]]} // limit where nodes can be dragged
706
+ />
707
+ ```
708
+
709
+ ### Connecting nodes
710
+
711
+ ```jsx
712
+ <InfiniteCanvas
713
+ nodesConnectable // default: true
714
+ connectionMode="loose" // 'strict' = only source→target, 'loose' = any handle
715
+ connectionRadius={20} // snap distance in pixels
716
+ connectOnClick // click handles instead of dragging
717
+ isValidConnection={(conn) => conn.source !== conn.target} // validation
718
+ defaultEdgeOptions={{ type: 'smoothstep', animated: true }} // defaults for new edges
719
+ onConnect={(connection) => ...}
720
+ />
721
+ ```
722
+
723
+ ### Selection
724
+
725
+ ```jsx
726
+ <InfiniteCanvas
727
+ elementsSelectable // default: true
728
+ multiSelectionKeyCode="Shift" // hold Shift to select multiple
729
+ selectionOnDrag // drag on empty space = selection box
730
+ selectionMode="partial" // 'partial' = touching box, 'full' = fully inside
731
+ onSelectionChange={({ nodes, edges }) => ...}
732
+ />
733
+ ```
734
+
735
+ ### Delete
736
+
737
+ ```jsx
738
+ <InfiniteCanvas
739
+ deleteKeyCode={['Delete', 'Backspace']} // keys that delete selected elements
740
+ onDelete={({ nodes, edges }) => console.log('Deleted:', nodes, edges)}
741
+ onBeforeDelete={async ({ nodes, edges }) => {
742
+ return window.confirm('Are you sure?'); // return false to cancel
743
+ }}
744
+ />
745
+ ```
746
+
747
+ ### Drag & Drop (external)
748
+
749
+ ```jsx
750
+ <InfiniteCanvas
751
+ onDragOver={(e) => e.preventDefault()}
752
+ onDrop={(e) => {
753
+ const type = e.dataTransfer.getData('application/reactflow');
754
+ const position = screenToFlowPosition({ x: e.clientX, y: e.clientY });
755
+ addNodes({ id: Date.now().toString(), type, position, data: { label: type } });
756
+ }}
757
+ />
758
+ ```
759
+
760
+ ---
761
+
762
+ ## Styling & Appearance
763
+
764
+ ### CSS
765
+
766
+ ```jsx
767
+ import '@infinit-canvas/react/styles.css'; // Required base styles
768
+ ```
769
+
770
+ ### Dark mode
771
+
772
+ ```jsx
773
+ <InfiniteCanvas dark /> // force dark
774
+ <InfiniteCanvas dark={false} /> // force light
775
+ // omit dark prop → auto-detect from system preference
776
+ ```
777
+
778
+ ### Background grid
779
+
780
+ ```jsx
781
+ import { Background } from '@infinit-canvas/react';
782
+
783
+ <InfiniteCanvas>
784
+ <Background variant="dots" gap={20} size={1} color="#aaa" />
785
+ </InfiniteCanvas>
786
+ ```
787
+
788
+ Variants: `'lines'` (default), `'dots'`, `'cross'`
789
+
790
+ ### Container sizing
791
+
792
+ ```jsx
793
+ <InfiniteCanvas width="100%" height="100vh" />
794
+ <InfiniteCanvas width={800} height={600} />
795
+ ```
796
+
797
+ ### HUD overlay
798
+
799
+ ```jsx
800
+ <InfiniteCanvas
801
+ showHud // shows world coords + zoom (default: true)
802
+ showHint // shows "Drag to pan - Scroll to zoom" hint (default: true)
803
+ hintText="Custom hint text"
804
+ />
805
+ ```
806
+
807
+ ---
808
+
809
+ ## Components
810
+
811
+ ### Controls
812
+
813
+ Zoom and fit-view buttons:
814
+
815
+ ```jsx
816
+ import { Controls } from '@infinit-canvas/react';
817
+
818
+ <InfiniteCanvas>
819
+ <Controls position="bottom-right" />
820
+ </InfiniteCanvas>
821
+ ```
822
+
823
+ ### MiniMap
824
+
825
+ Bird's-eye overview:
826
+
827
+ ```jsx
828
+ import { MiniMap } from '@infinit-canvas/react';
829
+
830
+ <InfiniteCanvas>
831
+ <MiniMap
832
+ width={200}
833
+ height={150}
834
+ nodeColor={(node) => node.selected ? '#ff0' : '#eee'}
835
+ />
836
+ </InfiniteCanvas>
837
+ ```
838
+
839
+ ### Panel
840
+
841
+ Position any content relative to the canvas:
842
+
843
+ ```jsx
844
+ import { Panel } from '@infinit-canvas/react';
845
+
846
+ <InfiniteCanvas>
847
+ <Panel position="top-left">
848
+ <h3>My Flow</h3>
849
+ </Panel>
850
+ </InfiniteCanvas>
851
+ ```
852
+
853
+ Positions: `'top-left'` | `'top-right'` | `'top-center'` | `'bottom-left'` | `'bottom-right'` | `'bottom-center'`
854
+
855
+ ### NodeResizer
856
+
857
+ Add resize handles to custom nodes:
858
+
859
+ ```jsx
860
+ import { NodeResizer } from '@infinit-canvas/react';
861
+
862
+ function ResizableNode({ data, selected }) {
863
+ return (
864
+ <>
865
+ <NodeResizer isVisible={selected} minWidth={100} minHeight={50} />
866
+ <div>{data.label}</div>
867
+ </>
868
+ );
869
+ }
870
+ ```
871
+
872
+ ### NodeToolbar
873
+
874
+ Floating toolbar that follows a node:
875
+
876
+ ```jsx
877
+ import { NodeToolbar } from '@infinit-canvas/react';
878
+
879
+ function MyNode({ data, id }) {
880
+ return (
881
+ <div>
882
+ <NodeToolbar position={Position.Top}>
883
+ <button>Edit</button>
884
+ <button>Delete</button>
885
+ </NodeToolbar>
886
+ {data.label}
887
+ </div>
888
+ );
889
+ }
890
+ ```
891
+
892
+ ### EdgeLabelRenderer
893
+
894
+ Portal for edge labels (renders in the edge label overlay layer):
895
+
896
+ ```jsx
897
+ import { EdgeLabelRenderer } from '@infinit-canvas/react';
898
+
899
+ function MyEdge({ sourceX, sourceY, targetX, targetY }) {
900
+ const midX = (sourceX + targetX) / 2;
901
+ const midY = (sourceY + targetY) / 2;
902
+
903
+ return (
904
+ <>
905
+ <path d={`M ${sourceX} ${sourceY} L ${targetX} ${targetY}`} stroke="#333" />
906
+ <EdgeLabelRenderer>
907
+ <div style={{
908
+ position: 'absolute',
909
+ transform: `translate(-50%, -50%) translate(${midX}px, ${midY}px)`,
910
+ pointerEvents: 'all',
911
+ }}>
912
+ Label
913
+ </div>
914
+ </EdgeLabelRenderer>
915
+ </>
916
+ );
917
+ }
918
+ ```
919
+
920
+ ### ViewportPortal
921
+
922
+ Render any React content that moves with the viewport:
923
+
924
+ ```jsx
925
+ import { ViewportPortal } from '@infinit-canvas/react';
926
+
927
+ <InfiniteCanvas>
928
+ <ViewportPortal>
929
+ <div style={{ position: 'absolute', left: 100, top: 100 }}>
930
+ I'm at world position (100, 100)
931
+ </div>
932
+ </ViewportPortal>
933
+ </InfiniteCanvas>
934
+ ```
935
+
936
+ ---
937
+
938
+ ## Utilities
939
+
940
+ ### applyNodeChanges(changes, nodes) → nodes
941
+
942
+ Apply an array of changes to your nodes array. Used in `onNodesChange`:
943
+
944
+ ```jsx
945
+ const onNodesChange = (changes) => setNodes((nds) => applyNodeChanges(changes, nds));
946
+ ```
947
+
948
+ ### applyEdgeChanges(changes, edges) → edges
949
+
950
+ Same for edges:
951
+
952
+ ```jsx
953
+ const onEdgesChange = (changes) => setEdges((eds) => applyEdgeChanges(changes, eds));
954
+ ```
955
+
956
+ ### addEdge(connection, edges) → edges
957
+
958
+ Add a new edge from a connection event:
959
+
960
+ ```jsx
961
+ const onConnect = (connection) => setEdges((eds) => addEdge(connection, eds));
962
+ ```
963
+
964
+ ### getConnectedEdges(nodes, edges) → edges
965
+
966
+ Find all edges connected to a set of nodes:
967
+
968
+ ```jsx
969
+ const connected = getConnectedEdges(selectedNodes, allEdges);
970
+ ```
971
+
972
+ ### getIncomers(node, nodes, edges) / getOutgoers(node, nodes, edges)
973
+
974
+ Find upstream/downstream nodes:
975
+
976
+ ```jsx
977
+ const parents = getIncomers(myNode, allNodes, allEdges);
978
+ const children = getOutgoers(myNode, allNodes, allEdges);
979
+ ```
980
+
981
+ ### getNodesBounds(nodes) → { x, y, width, height }
982
+
983
+ Compute the bounding box of a set of nodes:
984
+
985
+ ```jsx
986
+ const bounds = getNodesBounds(selectedNodes);
987
+ ```
988
+
989
+ ---
990
+
51
991
  ## Performance
52
992
 
53
- - **Spatial grid index** for O(1) viewport culling
54
- - **Batched multi-pass rendering** (~10 draw calls for any number of cards)
55
- - **Level of Detail** text/shadows hidden at low zoom
56
- - **Web Worker rendering** — main thread never blocked
57
- - **Pre-computed colors** and font constants
58
- - **Throttled HUD** updates (100ms)
993
+ This library is designed for large graphs (5000+ nodes). Here's how:
994
+
995
+ | Technique | What it does |
996
+ |-----------|-------------|
997
+ | **OffscreenCanvas Worker** | All drawing happens on a separate thread. Your React UI and event handlers never wait for rendering. |
998
+ | **Spatial grid culling** | Only visible nodes/edges are drawn. 5000 nodes on canvas but only 50 in view = only 50 drawn. |
999
+ | **Batched rendering** | All nodes drawn in 2-3 draw calls total (not one per node). Same for edges. |
1000
+ | **Camera as ref** | Panning/zooming never triggers React re-renders. Camera position is a mutable ref, not React state. |
1001
+ | **DOM transform sync** | Custom React nodes move via direct `element.style.transform` mutations in a rAF loop — no React reconciliation during pan/zoom. |
1002
+ | **Hit test skipping** | Mouse hover detection (O(n) scan) is skipped entirely during active pan, drag, connect, or selection. |
1003
+ | **Node virtualization** | Only visible custom React nodes are mounted in the DOM. Off-screen nodes are unmounted. |
1004
+ | **Grid tile caching** | Background grid is rendered once to a small canvas tile, then tiled via `createPattern('repeat')`. |
1005
+ | **Level of Detail** | Text labels, shadows, and arrows are skipped at low zoom levels where they'd be invisible anyway. |
1006
+ | **Edge routing** | Runs asynchronously after rendering, using spatial grid lookups for nearby obstacle nodes. |
1007
+ | **Incremental drag** | During drag, only changed node positions are sent to the worker — not the entire node array. |
1008
+
1009
+ ---
1010
+
1011
+ ## Full Props Reference
1012
+
1013
+ ### Data
1014
+
1015
+ | Prop | Type | Default | Description |
1016
+ |------|------|---------|-------------|
1017
+ | `nodes` | `Node[]` | `[]` | Array of nodes to display |
1018
+ | `edges` | `Edge[]` | `[]` | Array of edges connecting nodes |
1019
+ | `cards` | `Card[]` | `[]` | Legacy card-based data (simple rectangles) |
1020
+ | `nodeTypes` | `Record<string, Component>` | built-ins | Map of type name → React component for custom nodes |
1021
+ | `edgeTypes` | `Record<string, Component>` | built-ins | Map of type name → React component for custom edges |
1022
+
1023
+ ### Appearance
1024
+
1025
+ | Prop | Type | Default | Description |
1026
+ |------|------|---------|-------------|
1027
+ | `dark` | `boolean` | system | Force dark or light theme |
1028
+ | `gridSize` | `number` | `40` | Background grid spacing in world pixels |
1029
+ | `width` | `string \| number` | `'100%'` | Container width |
1030
+ | `height` | `string \| number` | `'420px'` | Container height |
1031
+ | `className` | `string` | `''` | CSS class on wrapper div |
1032
+ | `style` | `CSSProperties` | `{}` | Inline styles on wrapper div |
1033
+
1034
+ ### Camera
1035
+
1036
+ | Prop | Type | Default | Description |
1037
+ |------|------|---------|-------------|
1038
+ | `initialCamera` | `{ x, y, zoom }` | `{ x:0, y:0, zoom:1 }` | Starting camera position |
1039
+ | `fitView` | `boolean` | `false` | Auto-zoom to fit all nodes on mount |
1040
+ | `fitViewOptions` | `FitViewOptions` | — | Options for fitView (padding, maxZoom, etc.) |
1041
+ | `zoomMin` | `number` | `0.1` | Minimum zoom level |
1042
+ | `zoomMax` | `number` | `4` | Maximum zoom level |
1043
+ | `translateExtent` | `[[x,y],[x,y]]` | — | Camera movement bounds |
1044
+
1045
+ ### Behavior
1046
+
1047
+ | Prop | Type | Default | Description |
1048
+ |------|------|---------|-------------|
1049
+ | `nodesDraggable` | `boolean` | `true` | Can nodes be dragged? |
1050
+ | `nodesConnectable` | `boolean` | `true` | Can edges be created by dragging handles? |
1051
+ | `elementsSelectable` | `boolean` | `true` | Can nodes/edges be clicked to select? |
1052
+ | `snapToGrid` | `boolean` | `false` | Snap node positions to grid during drag |
1053
+ | `snapGrid` | `[number, number]` | `[15, 15]` | Grid spacing for snap |
1054
+ | `connectionMode` | `'strict' \| 'loose'` | `'loose'` | Strict: source→target only. Loose: any handle. |
1055
+ | `connectionRadius` | `number` | `20` | Snap-to-handle distance in pixels |
1056
+ | `connectOnClick` | `boolean` | `false` | Click handles to connect (instead of drag) |
1057
+ | `selectionOnDrag` | `boolean` | `false` | Drag on empty space creates selection box |
1058
+ | `selectionMode` | `'partial' \| 'full'` | `'partial'` | How selection box selects nodes |
1059
+ | `multiSelectionKeyCode` | `string` | `'Shift'` | Key to hold for multi-select |
1060
+ | `deleteKeyCode` | `string \| string[]` | `['Delete', 'Backspace']` | Keys that delete selected elements |
1061
+ | `panActivationKeyCode` | `string` | `' '` (space) | Key that activates pan mode |
1062
+ | `panOnScroll` | `boolean` | `false` | Scroll wheel pans instead of zooming |
1063
+ | `zoomOnScroll` | `boolean` | `true` | Scroll wheel zooms |
1064
+ | `zoomOnPinch` | `boolean` | `true` | Pinch gesture zooms |
1065
+ | `zoomOnDoubleClick` | `boolean` | `true` | Double-click zooms in |
1066
+ | `edgeRouting` | `boolean` | `true` | Smart edge paths that avoid nodes |
1067
+ | `edgesReconnectable` | `boolean` | `false` | Can existing edges be re-routed to different handles |
1068
+ | `elevateNodesOnSelect` | `boolean` | `false` | Bring selected nodes to front |
1069
+ | `autoPanOnNodeDrag` | `boolean` | `true` | Auto-scroll when dragging near edge |
1070
+ | `autoPanOnConnect` | `boolean` | `true` | Auto-scroll when connecting near edge |
1071
+ | `nodeExtent` | `[[x,y],[x,y]]` | — | Limit where nodes can be dragged |
1072
+
1073
+ ### Callbacks
1074
+
1075
+ | Prop | Signature | Description |
1076
+ |------|-----------|-------------|
1077
+ | `onNodesChange` | `(changes: NodeChange[]) => void` | Node was dragged, selected, added, or removed |
1078
+ | `onEdgesChange` | `(changes: EdgeChange[]) => void` | Edge was selected, added, or removed |
1079
+ | `onConnect` | `(connection: Connection) => void` | User connected two handles |
1080
+ | `onNodeClick` | `(event, node) => void` | Node was clicked |
1081
+ | `onNodeDoubleClick` | `(event, node) => void` | Node was double-clicked |
1082
+ | `onNodeDragStart` | `(event, node) => void` | Started dragging a node |
1083
+ | `onNodeDrag` | `(event, node) => void` | Dragging a node (fires every frame) |
1084
+ | `onNodeDragStop` | `(event, node) => void` | Finished dragging a node |
1085
+ | `onEdgeClick` | `(event, edge) => void` | Edge was clicked |
1086
+ | `onPaneClick` | `(event) => void` | Clicked empty canvas space |
1087
+ | `onSelectionChange` | `({ nodes, edges }) => void` | Selection changed |
1088
+ | `onInit` | `(instance) => void` | Canvas initialized (receives ReactFlowInstance) |
1089
+ | `onMove` | `(event, viewport) => void` | Camera moved (pan/zoom) |
1090
+ | `onDelete` | `({ nodes, edges }) => void` | Elements were deleted |
1091
+ | `onBeforeDelete` | `({ nodes, edges }) => Promise<boolean>` | Return false to prevent deletion |
1092
+ | `onConnect` | `(connection) => void` | New connection created |
1093
+ | `isValidConnection` | `(connection) => boolean` | Validate before allowing a connection |
1094
+
1095
+ ---
59
1096
 
60
1097
  ## License
61
1098