@infinit-canvas/react 0.1.10 → 0.1.12

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,99 +1,14 @@
1
1
  # @infinit-canvas/react
2
2
 
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.
3
+ [![npm version](https://img.shields.io/npm/v/@infinit-canvas/react.svg)](https://www.npmjs.com/package/@infinit-canvas/react)
4
+ [![license](https://img.shields.io/npm/l/@infinit-canvas/react.svg)](https://github.com/awaisshah228/infinit-canvas/blob/main/LICENSE)
4
5
 
5
- **Drop-in compatible with React Flow API**same props, hooks, and patterns. If you know React Flow, you already know this.
6
+ A high-performance infinite canvas React component powered by **OffscreenCanvas + Web Workers**. Drop-in React Flow compatible API — but renders on a canvas for **10x better performance** at scale.
6
7
 
7
- ---
8
+ **5,000+ nodes at 60fps** vs React Flow's ~500 before lag.
8
9
 
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
- ---
10
+ **npm:** [https://www.npmjs.com/package/@infinit-canvas/react](https://www.npmjs.com/package/@infinit-canvas/react)
11
+ **GitHub:** [https://github.com/awaisshah228/infinit-canvas](https://github.com/awaisshah228/infinit-canvas)
97
12
 
98
13
  ## Install
99
14
 
@@ -101,91 +16,25 @@ A lightweight state manager used internally. When you wrap your app in `<Infinit
101
16
  npm install @infinit-canvas/react
102
17
  ```
103
18
 
104
- ---
105
-
106
19
  ## Quick Start
107
20
 
108
- ### Simplest example — just nodes and edges
109
-
110
21
  ```jsx
111
- import { useState, useCallback } from 'react';
112
- import {
113
- InfiniteCanvas,
114
- applyNodeChanges,
115
- applyEdgeChanges,
116
- addEdge,
117
- } from '@infinit-canvas/react';
22
+ import { InfiniteCanvas, useNodesState, useEdgesState, addEdge, Controls, MiniMap, Background } from '@infinit-canvas/react';
118
23
  import '@infinit-canvas/react/styles.css';
119
24
 
120
- // Define your nodes — each needs a unique id and a position
121
25
  const initialNodes = [
122
- { id: '1', position: { x: 0, y: 0 }, data: { label: 'Start' } },
123
- { id: '2', position: { x: 200, y: 100 }, data: { label: 'End' } },
26
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } },
27
+ { id: '2', position: { x: 250, y: 100 }, data: { label: 'Node 2' } },
124
28
  ];
125
29
 
126
- // Define your edges — each connects a source node to a target node
127
30
  const initialEdges = [
128
31
  { id: 'e1-2', source: '1', target: '2' },
129
32
  ];
130
33
 
131
34
  function App() {
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
- );
164
- }
165
- ```
166
-
167
- ### Even simpler — with helper hooks
168
-
169
- ```jsx
170
- import {
171
- InfiniteCanvas,
172
- useNodesState,
173
- useEdgesState,
174
- addEdge,
175
- } from '@infinit-canvas/react';
176
- import '@infinit-canvas/react/styles.css';
177
-
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
35
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
188
36
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
37
+ const onConnect = (conn) => setEdges((eds) => addEdge(conn, eds));
189
38
 
190
39
  return (
191
40
  <InfiniteCanvas
@@ -193,907 +42,123 @@ function App() {
193
42
  edges={edges}
194
43
  onNodesChange={onNodesChange}
195
44
  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:
302
-
303
- ```jsx
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
- ];
351
-
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>
45
+ onConnect={onConnect}
46
+ height="500px"
47
+ >
48
+ <Controls />
49
+ <MiniMap />
50
+ <Background variant="dots" />
51
+ </InfiniteCanvas>
463
52
  );
464
53
  }
465
54
  ```
466
55
 
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()
56
+ ## Why This?
512
57
 
513
- Subscribe to nodes/edges re-renders when they change.
58
+ | | React Flow | @infinit-canvas/react |
59
+ |---|---|---|
60
+ | Rendering | DOM (React divs + SVG) | OffscreenCanvas + Web Worker |
61
+ | Max nodes (60fps) | ~500 | 5,000+ |
62
+ | Main thread | Blocked during render | Always free |
63
+ | Custom nodes | React components | Hybrid: canvas + DOM overlay |
64
+ | API | React Flow API | Same API (drop-in compatible) |
514
65
 
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
- ```
66
+ ## Features
534
67
 
535
- ### useViewport()
68
+ - **Full React Flow API** — nodes, edges, handles, connections, selection, drag, zoom, pan
69
+ - **Custom node & edge types** — use any React component
70
+ - **Built-in components** — Controls, MiniMap, Background, Panel, Handle, NodeResizer, NodeToolbar, EdgeLabelRenderer, ViewportPortal
71
+ - **All hooks** — useReactFlow, useNodes, useEdges, useViewport, useConnection, useNodesData, useOnViewportChange, useOnSelectionChange, useKeyPress, useStore, useStoreApi, + more
72
+ - **Sub-flows** — parentId, extent: 'parent'
73
+ - **Connection modes** — strict/loose, click-to-connect, validation
74
+ - **Selection** — click, Shift+click multi-select, Ctrl+A, selection box, selection drag events
75
+ - **Edge types** — bezier, straight, step, smoothstep, animated, labels
76
+ - **Edge routing** — automatic obstacle avoidance
77
+ - **Snap to grid** — configurable grid snapping
78
+ - **Undo/redo** — built-in history management
79
+ - **Delete validation** — onBeforeDelete async callback
80
+ - **Error handling** — onError callback
81
+ - **noDragClassName / noPanClassName** — prevent drag/pan on specific elements
82
+ - **Elevate on select** — bring nodes/edges to front on selection
83
+ - **Node virtualization** — only visible custom nodes are mounted in DOM
84
+ - **Frustum culling** — spatial grid index for O(visible) rendering
536
85
 
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');
596
- ```
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`) |
86
+ ## Props
676
87
 
677
88
  ```jsx
678
89
  <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
90
+ // Data
91
+ nodes={nodes}
92
+ edges={edges}
93
+ onNodesChange={onNodesChange}
94
+ onEdgesChange={onEdgesChange}
95
+ onConnect={onConnect}
96
+
97
+ // Types
98
+ nodeTypes={nodeTypes}
99
+ edgeTypes={edgeTypes}
100
+
101
+ // Events
102
+ onNodeClick onNodeDoubleClick onNodeContextMenu
103
+ onNodeDragStart onNodeDrag onNodeDragStop
104
+ onEdgeClick onEdgeDoubleClick
105
+ onPaneClick onPaneContextMenu
106
+ onSelectionChange
107
+ onSelectionDragStart onSelectionDrag onSelectionDragStop
108
+ onConnectStart onConnectEnd
109
+ onMoveStart onMove onMoveEnd
110
+ onInit onDelete onBeforeDelete onError
111
+
112
+ // Behavior
113
+ nodesDraggable nodesConnectable elementsSelectable
114
+ connectionMode="loose" connectOnClick={false}
115
+ snapToGrid snapGrid={[15, 15]}
116
+ elevateNodesOnSelect elevateEdgesOnSelect
117
+ noDragClassName="nodrag" noPanClassName="nopan"
118
+ deleteKeyCode="Delete"
119
+ isValidConnection={fn}
120
+
121
+ // Viewport
122
+ fitView initialCamera={{ x: 0, y: 0, zoom: 1 }}
123
+ zoomMin={0.1} zoomMax={4}
124
+ panOnScroll zoomOnScroll zoomOnPinch zoomOnDoubleClick
125
+ translateExtent autoPanOnNodeDrag
126
+
127
+ // Appearance
128
+ dark gridSize={40} width="100%" height="500px"
683
129
  />
684
130
  ```
685
131
 
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
- ```
132
+ ## Hooks
935
133
 
936
- ---
134
+ | Hook | Description |
135
+ |---|---|
136
+ | `useNodesState` | useState + applyNodeChanges |
137
+ | `useEdgesState` | useState + applyEdgeChanges |
138
+ | `useReactFlow` | Imperative API (fitView, zoomIn, getNodes, setNodes, etc.) |
139
+ | `useNodes` / `useEdges` | Read current nodes/edges |
140
+ | `useViewport` | Read { x, y, zoom } |
141
+ | `useConnection` | Connection state while dragging |
142
+ | `useOnViewportChange` | Subscribe to viewport changes |
143
+ | `useOnSelectionChange` | Subscribe to selection changes |
144
+ | `useKeyPress` | Track key state |
145
+ | `useStore` / `useStoreApi` | Direct store access |
146
+ | `useUndoRedo` | Undo/redo history |
937
147
 
938
148
  ## Utilities
939
149
 
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
-
991
- ## Performance
992
-
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 |
150
+ `applyNodeChanges` · `applyEdgeChanges` · `addEdge` · `isNode` · `isEdge` · `getConnectedEdges` · `getIncomers` · `getOutgoers` · `getNodesBounds` · `getBezierPath` · `getSmoothStepPath` · `getStraightPath` · `getSimpleBezierPath` · `snapPosition` · `reconnectEdge`
1044
151
 
1045
- ### Behavior
152
+ ## Sponsor
1046
153
 
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 |
154
+ If this package saves you time, consider supporting its development:
1072
155
 
1073
- ### Callbacks
156
+ [![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?logo=github)](https://github.com/sponsors/awaisshah228)
1074
157
 
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 |
158
+ **USDC (Solana):** `59FhVxK3uxABiJ9VzXtCoyCxqq4nhoZDBtUV3gEkiexo`
1094
159
 
1095
- ---
160
+ <img src="https://raw.githubusercontent.com/awaisshah228/react-flow-avoid-nodes-routing/turbo-package/assets/solana-donate-qr.png" width="200" alt="Solana USDC QR Code" />
1096
161
 
1097
162
  ## License
1098
163
 
1099
- MIT
164
+ [MIT](./LICENSE)