# @blinksgg/canvas - LLM API Reference (v3.0)

> A batteries-included canvas library for node-based editors with drag, resize, pan/zoom, selection, undo/redo, and Supabase sync. Uses **React 19.2+** with the **React Compiler** — zero manual `useCallback`/`useMemo` in the codebase.

## Quick Start

```tsx
import {
  Canvas,
  CanvasProvider,
  CanvasDbProvider,
  registerNodeTypes,
  useDeleteNode,
  useCreateNode,
} from '@blinksgg/canvas';

// 1. Register your node type components
registerNodeTypes({
  'my-node': MyNodeComponent,
});

// 2. Wrap your app with providers
<CanvasDbProvider supabaseUrl={url} supabaseAnonKey={key}>
  <CanvasProvider graphId={graphId}>
    <Canvas />
  </CanvasProvider>
</CanvasDbProvider>
```

---

## CRUD Operations (Database Hooks)

These hooks handle database operations with optimistic updates:

| Hook | Purpose | Usage |
|------|---------|-------|
| `useCreateNode` | Create a new node | `const { mutate } = useCreateNode()` |
| `useUpdateNode` | Update node properties | `const { mutate } = useUpdateNode()` |
| `useDeleteNode` | Delete a node | `const { mutate } = useDeleteNode()` |
| `useCreateEdge` | Create an edge between nodes | `const { mutate } = useCreateEdge()` |
| `useUpdateEdge` | Update edge properties | `const { mutate } = useUpdateEdge()` |
| `useDeleteEdge` | Delete an edge | `const { mutate } = useDeleteEdge()` |
| `useGraphNodes` | Fetch all nodes for a graph | `const { data } = useGraphNodes(graphId)` |
| `useGraphEdges` | Fetch all edges for a graph | `const { data } = useGraphEdges(graphId)` |

### Example: Delete a Node

```tsx
import { useDeleteNode } from '@blinksgg/canvas';

function MyComponent() {
  const deleteNode = useDeleteNode();

  const handleDelete = (nodeId: string, graphId: string) => {
    deleteNode.mutate({ nodeId, graphId });
  };
}
```

### Example: Create a Node

```tsx
import { useCreateNode } from '@blinksgg/canvas';

function MyComponent() {
  const createNode = useCreateNode();

  const handleCreate = () => {
    createNode.mutate({
      graphId: 'my-graph-id',
      node: {
        label: 'New Node',
        node_type: 'my-node',
        ui_properties: { x: 100, y: 100, width: 200, height: 150 },
      },
    });
  };
}
```

---

## Selection Management

### Atoms (for Jotai)

| Atom | Purpose |
|------|---------|
| `selectedNodeIdsAtom` | Set of selected node IDs |
| `selectedEdgeIdAtom` | Currently selected edge ID |
| `hasSelectionAtom` | Boolean: any nodes selected? |
| `selectedNodesCountAtom` | Number of selected nodes |

### Actions

| Atom | Purpose |
|------|---------|
| `selectSingleNodeAtom` | Select only one node |
| `toggleNodeInSelectionAtom` | Toggle node in/out of selection |
| `addNodesToSelectionAtom` | Add nodes to selection |
| `removeNodesFromSelectionAtom` | Remove nodes from selection |
| `clearSelectionAtom` | Clear all selection |
| `selectEdgeAtom` | Select an edge |
| `clearEdgeSelectionAtom` | Clear edge selection |

### Hook

```tsx
import { useCanvasSelection } from '@blinksgg/canvas';

function MyComponent() {
  const { selectedNodeIds, count, hasSelection, hasEdgeSelection } = useCanvasSelection();
}
```

---

## Virtualization (Performance)

Canvas uses viewport virtualization to only render nodes and edges visible on screen. This dramatically improves performance for large graphs (100+ nodes).

### How It Works

- Only nodes within the viewport bounds (plus 200px buffer) are rendered
- Edges are only rendered if both source and target nodes are visible
- Virtualization is enabled by default and persisted to localStorage

### Settings Integration

Virtualization is controlled through the canvas settings system, which persists user preferences:

```tsx
import { useCanvasSettings } from '@blinksgg/canvas';

function VirtualizationToggle() {
  const settings = useCanvasSettings();
  // Access virtualizationEnabled from settings
}
```

### Atoms

| Atom | Purpose |
|------|---------|
| `virtualizationEnabledAtom` | Whether virtualization is on (persisted) |
| `setVirtualizationEnabledAtom` | Set virtualization enabled/disabled |
| `toggleVirtualizationAtom` | Toggle virtualization on/off |
| `visibleBoundsAtom` | Current visible area in world coords |
| `visibleNodeKeysAtom` | Node IDs visible in viewport |
| `visibleEdgeKeysAtom` | Edge keys visible in viewport |
| `virtualizationMetricsAtom` | Debug metrics |

### Hook

```tsx
import { useVirtualization } from '@blinksgg/canvas';

function PerformanceMonitor() {
  const {
    enabled,          // Is virtualization on?
    totalNodes,       // Total nodes in graph
    visibleNodes,     // Nodes being rendered
    culledNodes,      // Nodes not rendered (off-screen)
    totalEdges,
    visibleEdges,
    culledEdges,
    bounds,           // Current visible bounds { minX, minY, maxX, maxY }
    enable,           // Enable virtualization
    disable,          // Disable (render all)
    toggle,           // Toggle on/off
  } = useVirtualization();

  return (
    <div>
      Rendering {visibleNodes}/{totalNodes} nodes
      ({culledNodes} culled)
    </div>
  );
}
```

### Disable Virtualization

For debugging or small graphs:

```tsx
import { useSetAtom } from 'jotai';
import { setVirtualizationEnabledAtom, toggleVirtualizationAtom } from '@blinksgg/canvas';

// Set directly
const setEnabled = useSetAtom(setVirtualizationEnabledAtom);
setEnabled(false); // Render all nodes

// Or toggle
const toggle = useSetAtom(toggleVirtualizationAtom);
toggle(); // Toggle on/off
```

---

## Viewport (Pan/Zoom)

### Atoms

| Atom | Purpose |
|------|---------|
| `zoomAtom` | Current zoom level (0.1 to 3) |
| `panAtom` | Current pan offset { x, y } |
| `viewportRectAtom` | Viewport dimensions |
| `screenToWorldAtom` | Convert screen coords to world |
| `worldToScreenAtom` | Convert world coords to screen |

### Actions

| Atom | Purpose |
|------|---------|
| `setZoomAtom` | Set zoom level |
| `resetViewportAtom` | Reset to default view |

### Hook

```tsx
import { useCanvasViewport } from '@blinksgg/canvas';

function MyComponent() {
  const { zoom, pan, screenToWorld, worldToScreen } = useCanvasViewport();
}
```

---

## Layout (Fit-to-View, Force Layout, Tree, Grid)

### Layout Hooks

| Hook | Purpose |
|------|---------|
| `useLayout` | Combined layout utilities (fitToBounds, bounds) |
| `useFitToBounds` | Fit viewport to show nodes |
| `useGetGraphBounds` | Calculate bounding rect of all nodes |
| `useSelectionBounds` | Calculate bounding rect of selected nodes |
| `useForceLayout` | Apply force-directed layout to arrange nodes |
| `useTreeLayout` | Hierarchical tree layout (top-down or left-right) |
| `useGridLayout` | Uniform grid layout (auto columns, spatial sort) |
| `useAnimatedLayout` | Shared animated position interpolation hook |

### Fit to Bounds

Automatically zoom and pan to show all nodes or selection:

```tsx
import { useFitToBounds, FitToBoundsMode } from '@blinksgg/canvas/hooks';

function MyComponent() {
  const { fitToBounds } = useFitToBounds();

  // Fit all nodes in view with 50px padding
  const handleFitAll = () => {
    fitToBounds(FitToBoundsMode.Graph, 50);
  };

  // Fit only selected nodes
  const handleFitSelection = () => {
    fitToBounds(FitToBoundsMode.Selection, 20);
  };
}
```

### Graph Bounds

Get the bounding rectangle of all nodes:

```tsx
import { useGetGraphBounds } from '@blinksgg/canvas/hooks';

function MyComponent() {
  const { bounds, nodes } = useGetGraphBounds();

  console.log(`Graph spans ${bounds.width}x${bounds.height}`);
  console.log(`Top-left at (${bounds.x}, ${bounds.y})`);
}
```

### Force Layout

Apply D3 force-directed layout to arrange nodes without overlap:

```tsx
import { useForceLayout } from '@blinksgg/canvas/hooks';

function MyComponent() {
  // Basic usage (no persistence)
  const { applyForceLayout } = useForceLayout();

  const handleArrange = async () => {
    await applyForceLayout();
    // Nodes are now arranged without overlap
  };
}
```

With database persistence callback:

```tsx
import { useForceLayout, type NodePositionUpdate } from '@blinksgg/canvas/hooks';

function MyComponent() {
  const { applyForceLayout } = useForceLayout({
    onPositionsChanged: async (updates: NodePositionUpdate[]) => {
      // Save all positions to your database
      for (const { nodeId, position } of updates) {
        await savePosition(nodeId, position);
      }
    },
    maxIterations: 1000,      // Default: 1000
    chargeStrength: -6000,    // Default: -6000 (more negative = stronger repulsion)
    linkStrength: 0.03,       // Default: 0.03
  });
}
```

### Layout Utilities (Pure Functions)

Available from `@blinksgg/canvas/utils`:

| Function | Purpose |
|----------|---------|
| `calculateBounds(nodes)` | Calculate bounding rect from array of Rect |
| `checkNodesOverlap(node1, node2)` | Check if two nodes overlap |
| `getNodeCenter(node)` | Get center point of a node |
| `setNodeCenter(node, x, y)` | Set node position by center |
| `getNodeBounds(node)` | Get full bounds with edges |
| `areNodesClose(node1, node2, threshold)` | Check if nodes are within distance |

```tsx
import { calculateBounds, checkNodesOverlap, type Rect } from '@blinksgg/canvas/utils';

const nodes: Rect[] = [
  { x: 0, y: 0, width: 100, height: 100 },
  { x: 200, y: 150, width: 100, height: 100 },
];

const bounds = calculateBounds(nodes);
// bounds = { x: 0, y: 0, width: 300, height: 250 }

const overlaps = checkNodesOverlap(nodes[0], nodes[1]);
// overlaps = false
```

---

## Undo/Redo (History)

### Atoms

| Atom | Purpose |
|------|---------|
| `canUndoAtom` | Boolean: can undo? |
| `canRedoAtom` | Boolean: can redo? |
| `undoCountAtom` | Number of undo steps available |
| `redoCountAtom` | Number of redo steps available |
| `historyLabelsAtom` | Labels for history entries |

### Actions

| Atom | Purpose |
|------|---------|
| `pushHistoryAtom` | Push current state to history |
| `undoAtom` | Undo last action |
| `redoAtom` | Redo last undone action |
| `clearHistoryAtom` | Clear history stack |

### Hook

```tsx
import { useCanvasHistory } from '@blinksgg/canvas';

function MyComponent() {
  const { undo, redo, canUndo, canRedo, pushHistory } = useCanvasHistory();

  // Before making changes, save state
  pushHistory('Move nodes');

  // User can then undo/redo
  if (canUndo) undo();
  if (canRedo) redo();
}
```

---

## Node Drag & Resize

### Hooks

```tsx
import { useNodeDrag, useNodeResize } from '@blinksgg/canvas';

// For drag functionality
const { bind, isDragging } = useNodeDrag(nodeId, nodeData);

// For resize functionality
const { localWidth, localHeight, isResizing, createResizeStart } = useNodeResize({
  id: nodeId,
  nodeData,
  updateNodePositions,
});
```

---

## Node Type Registry

Register custom components for different node types:

```tsx
import { registerNodeType, registerNodeTypes, getNodeTypeComponent } from '@blinksgg/canvas';

// Register single type
registerNodeType('note', NoteNodeComponent);

// Register multiple types
registerNodeTypes({
  'note': NoteNodeComponent,
  'image': ImageNodeComponent,
  'code': CodeNodeComponent,
});

// Check if type exists
const hasType = hasNodeTypeComponent('note');

// Get all registered types
const types = getRegisteredNodeTypes(); // ['note', 'image', 'code']
```

---

## Bundled Node Components

### NoteNode

A rich text editor node using BlockNote:

```tsx
import { NoteNode, type NoteNodeStorage } from '@blinksgg/canvas';

// Implement storage adapter
const storage: NoteNodeStorage = {
  content: htmlContent,
  onChange: (html) => saveToBackend(html),
  subscribe: (callback) => subscribeToChanges(callback),
  isLoading: false,
};

<NoteNode nodeData={nodeData} storage={storage} theme="light" />
```

---

## Components

| Component | Purpose |
|-----------|---------|
| `Canvas` | Main canvas container |
| `CanvasProvider` | Provides canvas context |
| `CanvasDbProvider` | Provides Supabase client |
| `CanvasStyleProvider` | Theming and styles |
| `InputProvider` / `GestureProvider` | Gesture system v2 context |
| `Viewport` | Pan/zoom container |
| `NodeRenderer` | Renders all nodes |
| `Node` | Single node wrapper |
| `GroupNode` | Collapsible group container |
| `ConnectedNode` | Node with connections |
| `EdgeRenderer` | Renders all edges |
| `Grid` | Background grid |
| `Minimap` | Canvas overview with draggable viewport |
| `SelectionOverlay` | Lasso/rect selection |
| `ViewportControls` | Zoom +/- buttons |
| `ResizeHandle` | Node resize handles |
| `NodeContextMenu` | Right-click menu |
| `NodeErrorBoundary` | Error boundary for nodes |
| `LockedNodeOverlay` | Full-screen node view |
| `EdgeOverlay` | Edge creation overlay |
| `EdgeLabelEditor` | Inline edge label editing |
| `AlignmentGuides` | Snap alignment lines |
| `CanvasAnimations` | CSS animation injection |
| `NodeTypeCombobox` | Combobox to select and add nodes |
| `CommandLine` | Command palette UI |
| `CommandFeedbackOverlay` | Command input feedback |

---

## Graph State (Core Atoms)

| Atom | Purpose |
|------|---------|
| `graphAtom` | Graphology graph instance |
| `currentGraphIdAtom` | Active graph ID |
| `uiNodesAtom` | All nodes as UINodeState[] |
| `nodeKeysAtom` | Array of node IDs |
| `edgeKeysAtom` | Array of edge keys |

### Local Graph Mutations (Optimistic)

| Atom | Purpose |
|------|---------|
| `addNodeToLocalGraphAtom` | Add node to local graph |
| `addEdgeToLocalGraphAtom` | Add edge to local graph |
| `removeEdgeFromLocalGraphAtom` | Remove edge from local graph |
| `optimisticDeleteNodeAtom` | Optimistically delete node |
| `optimisticDeleteEdgeAtom` | Optimistically delete edge |

---

## Sync Status

| Atom | Purpose |
|------|---------|
| `syncStatusAtom` | 'synced' | 'syncing' | 'error' | 'offline' |
| `isOnlineAtom` | Boolean: is online? |
| `pendingMutationsCountAtom` | Count of pending mutations |
| `lastSyncErrorAtom` | Last sync error message |
| `mutationQueueAtom` | Queue of pending mutations |

---

## Locked Node (Full-screen View)

| Atom | Purpose |
|------|---------|
| `lockedNodeIdAtom` | ID of locked node |
| `lockedNodeDataAtom` | Data of locked node |
| `hasLockedNodeAtom` | Boolean: is a node locked? |
| `lockNodeAtom` | Lock a node (full-screen) |
| `unlockNodeAtom` | Unlock the node |

---

## Direct Query Functions

For advanced use cases, direct Supabase queries:

```tsx
import {
  fetchGraphNodes,
  fetchGraphNode,
  createGraphNode,
  updateGraphNode,
  deleteGraphNode,
  fetchGraphEdges,
  createGraphEdge,
  updateGraphEdge,
  deleteGraphEdge,
} from '@blinksgg/canvas';

// Use with your own Supabase client
const nodes = await fetchGraphNodes(supabase, graphId);
await deleteGraphNode(supabase, nodeId);
```

---

## Types

Key TypeScript types:

```tsx
import type {
  // Node types
  DBGraphNode,
  UINodeState,
  NodeId,
  GraphNodeAttributes,
  NodePosition,

  // Edge types
  DBGraphEdge,
  UIEdgeState,
  EdgeKey,
  GraphEdgeAttributes,

  // Component props
  NodeTypeComponentProps,
  NodeRenderProps,
  CanvasProps,

  // Storage adapters
  NoteNodeStorage,
  NoteNodeProps,
} from '@blinksgg/canvas';
```

---

## Import Paths

```tsx
// Main entry (everything)
import { ... } from '@blinksgg/canvas';

// Specific subpaths
import { ... } from '@blinksgg/canvas/core';      // State atoms only
import { ... } from '@blinksgg/canvas/db';        // Database layer
import { ... } from '@blinksgg/canvas/hooks';     // React hooks
import { ... } from '@blinksgg/canvas/nodes';     // Bundled node components
import { ... } from '@blinksgg/canvas/utils';     // Layout utilities, geometry functions
import { ... } from '@blinksgg/canvas/gestures';  // Gesture system v2 pipeline
import { ... } from '@blinksgg/canvas/commands';  // Command palette system
```

### Peer Dependencies

| Package | Version |
|---------|---------|
| `react` / `react-dom` | ^19.2 |
| `jotai` | ^2.6 |
| `d3-force` | ^3.0 (optional) |
| `@tanstack/react-query` | ^5.17 (optional) |

> **React Compiler**: The codebase has zero manual `useCallback`/`useMemo`/`React.memo`. The React Compiler (`babel-plugin-react-compiler`) auto-memoizes everything at build time.

---

## Common Patterns

### Delete Selected Nodes

```tsx
import { useDeleteNode, useCanvasSelection } from '@blinksgg/canvas';
import { useAtomValue } from 'jotai';
import { currentGraphIdAtom } from '@blinksgg/canvas';

function DeleteButton() {
  const deleteNode = useDeleteNode();
  const { selectedNodeIds } = useCanvasSelection();
  const graphId = useAtomValue(currentGraphIdAtom);

  const handleDeleteSelected = () => {
    selectedNodeIds.forEach(nodeId => {
      deleteNode.mutate({ nodeId, graphId });
    });
  };

  return <button onClick={handleDeleteSelected}>Delete Selected</button>;
}
```

### Create Node at Click Position

```tsx
import { useCreateNode, screenToWorldAtom } from '@blinksgg/canvas';
import { useAtomValue } from 'jotai';

function handleCanvasClick(e: React.MouseEvent, graphId: string) {
  const screenToWorld = useAtomValue(screenToWorldAtom);
  const createNode = useCreateNode();

  const worldPos = screenToWorld({ x: e.clientX, y: e.clientY });

  createNode.mutate({
    graphId,
    node: {
      label: 'New Node',
      node_type: 'default',
      ui_properties: { x: worldPos.x, y: worldPos.y, width: 200, height: 150 },
    },
  });
}
```

### Undo/Redo with Keyboard

```tsx
import { useCanvasHistory } from '@blinksgg/canvas';
import { useEffect } from 'react';

function UndoRedoKeyboard() {
  const { undo, redo, canUndo, canRedo } = useCanvasHistory();

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.metaKey || e.ctrlKey) {
        if (e.key === 'z' && !e.shiftKey && canUndo) {
          e.preventDefault();
          undo();
        }
        if ((e.key === 'z' && e.shiftKey || e.key === 'y') && canRedo) {
          e.preventDefault();
          redo();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  });
  // React Compiler auto-tracks dependencies — no dep array needed

  return null;
}
```

---

## Plugin System

Register canvas plugins that provide custom node types, actions, or behaviors:

```tsx
import { usePlugin, usePlugins } from '@blinksgg/canvas';
import type { CanvasPlugin } from '@blinksgg/canvas';

const myPlugin: CanvasPlugin = {
  id: 'my-plugin',
  name: 'My Plugin',
  nodeTypes: { 'custom': CustomNode },
};

// Single plugin
function App() {
  usePlugin(myPlugin);
  return <Canvas />;
}

// Multiple plugins (dependencies should come first)
function App() {
  usePlugins([basePlugin, extensionPlugin]);
  return <Canvas />;
}
```

---

## Delete Operations

Delete nodes and edges using either database hooks (persisted) or optimistic atoms (local-only).

### Database Hooks (Persisted Delete)

| Hook | Purpose |
|------|---------|
| `useDeleteNode` | Delete node from database |
| `useDeleteEdge` | Delete edge from database |

```tsx
import { useDeleteNode, useDeleteEdge } from '@blinksgg/canvas';

function MyComponent() {
  const deleteNode = useDeleteNode();
  const deleteEdge = useDeleteEdge();

  // Delete a node (also removes connected edges)
  deleteNode.mutate({ nodeId: 'node-123', graphId: 'graph-id' });

  // Delete an edge
  deleteEdge.mutate({ edgeId: 'edge-456', graphId: 'graph-id' });
}
```

### Optimistic Atoms (Local-Only Delete)

For local-only operations without database sync:

| Atom | Purpose |
|------|---------|
| `optimisticDeleteNodeAtom` | Remove node from local graph instantly |
| `optimisticDeleteEdgeAtom` | Remove edge from local graph instantly |

```tsx
import { useSetAtom } from 'jotai';
import { optimisticDeleteNodeAtom, optimisticDeleteEdgeAtom } from '@blinksgg/canvas';

function MyComponent() {
  const deleteNode = useSetAtom(optimisticDeleteNodeAtom);
  const deleteEdge = useSetAtom(optimisticDeleteEdgeAtom);

  // Delete node from local graph (no database sync)
  deleteNode('node-123');

  // Delete edge from local graph (no database sync)
  deleteEdge('source-id', 'target-id');
}
```

### Delete with Selection

```tsx
import { useDeleteNode, useCanvasSelection, currentGraphIdAtom } from '@blinksgg/canvas';
import { useAtomValue } from 'jotai';

function DeleteSelectedButton() {
  const deleteNode = useDeleteNode();
  const { selectedNodeIds } = useCanvasSelection();
  const graphId = useAtomValue(currentGraphIdAtom);

  const handleDelete = () => {
    selectedNodeIds.forEach(nodeId => {
      deleteNode.mutate({ nodeId, graphId });
    });
  };

  return <button onClick={handleDelete}>Delete Selected</button>;
}
```

### Keyboard Shortcut for Delete

```tsx
import { useDeleteNode, useCanvasSelection, currentGraphIdAtom } from '@blinksgg/canvas';
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';

function DeleteKeyHandler() {
  const deleteNode = useDeleteNode();
  const { selectedNodeIds } = useCanvasSelection();
  const graphId = useAtomValue(currentGraphIdAtom);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNodeIds.length > 0) {
        e.preventDefault();
        selectedNodeIds.forEach(nodeId => {
          deleteNode.mutate({ nodeId, graphId });
        });
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [deleteNode, selectedNodeIds, graphId]);

  return null;
}
```

---

## NodeTypeCombobox (Add Node UI)

A searchable combobox for selecting and adding nodes to the canvas.

### Basic Usage

```tsx
import { NodeTypeCombobox } from '@blinksgg/canvas';

// Uses all registered node types automatically
<NodeTypeCombobox onNodeCreated={(id, type) => console.log('Created:', id, type)} />
```

### With Custom Node Types

```tsx
import { NodeTypeCombobox, type NodeTypeOption } from '@blinksgg/canvas';

const nodeTypes: NodeTypeOption[] = [
  { type: 'note', label: 'Note', icon: '📝', description: 'Rich text note' },
  { type: 'image', label: 'Image', icon: '🖼️', description: 'Image container' },
  { type: 'code', label: 'Code', icon: '💻', defaultWidth: 400, defaultHeight: 300 },
];

<NodeTypeCombobox
  nodeTypes={nodeTypes}
  placeholder="Add a node..."
  createPosition="center"  // or 'random' or { x: 100, y: 200 }
  onNodeCreated={(id) => selectNode(id)}
/>
```

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `nodeTypes` | `NodeTypeOption[]` | registered types | Custom list of node types |
| `onNodeCreated` | `(id, type) => void` | - | Callback after creation |
| `createPosition` | `'center' \| 'random' \| {x,y}` | `'center'` | Where to place new node |
| `placeholder` | `string` | `'Add node...'` | Input placeholder |
| `clearOnCreate` | `boolean` | `true` | Clear search after create |
| `disabled` | `boolean` | `false` | Disable the combobox |

### NodeTypeOption Interface

```tsx
interface NodeTypeOption {
  type: string;           // Node type identifier
  label?: string;         // Display label
  icon?: string;          // Emoji or icon
  description?: string;   // Shown in dropdown
  defaultWidth?: number;  // Default: 200
  defaultHeight?: number; // Default: 150
}
```

---

## Settings Panel (Event-Action Mappings)

Configure what actions trigger on canvas events (double-click node → fit to view, etc.).

### Basic Usage

```tsx
import { SettingsPanel, useCanvasSettings } from '@blinksgg/canvas';

function MyApp() {
  const { isPanelOpen, togglePanel } = useCanvasSettings();

  return (
    <>
      <button onClick={togglePanel}>Settings</button>
      {isPanelOpen && <SettingsPanel onClose={togglePanel} />}
    </>
  );
}
```

### Canvas Events

| Event | Description |
|-------|-------------|
| `node:double-click` | Double-click on a node |
| `node:triple-click` | Triple-click on a node |
| `node:right-click` | Right-click on a node |
| `node:long-press` | Long-press on a node (touch) |
| `background:click` | Click on canvas background |
| `background:double-click` | Double-click on background |
| `background:right-click` | Right-click on background |
| `background:long-press` | Long-press on background (touch) |

### Built-in Actions

| Action ID | Description |
|-----------|-------------|
| `none` | Do nothing |
| `select-node` | Select the node |
| `add-to-selection` | Add node to selection |
| `clear-selection` | Clear all selection |
| `delete-selected` | Delete selected nodes |
| `fit-to-view` | Fit node in viewport |
| `fit-all-to-view` | Fit all nodes |
| `center-on-node` | Center viewport on node |
| `lock-node` | Lock the node |
| `unlock-node` | Unlock the node |
| `toggle-lock` | Toggle node lock |
| `open-context-menu` | Open context menu |
| `apply-force-layout` | Apply D3 force layout |
| `undo` | Undo last action |
| `redo` | Redo last action |
| `create-node` | Create a new node |

### useCanvasSettings Hook

```tsx
import { useCanvasSettings } from '@blinksgg/canvas';

function SettingsManager() {
  const {
    mappings,           // Current event-action mappings
    activePresetId,     // ID of active preset (null if modified)
    allPresets,         // Built-in + custom presets
    hasUnsavedChanges,  // True if mappings differ from preset
    isPanelOpen,        // Panel visibility state
    setEventMapping,    // Set action for an event
    applyPreset,        // Apply a preset
    saveAsPreset,       // Save current mappings as preset
    deletePreset,       // Delete a custom preset
    resetSettings,      // Reset to defaults
    togglePanel,        // Toggle panel visibility
  } = useCanvasSettings();
}
```

### useActionExecutor Hook

Execute actions programmatically with access to canvas state:

```tsx
import { useActionExecutor, CanvasEventType } from '@blinksgg/canvas';

function MyCanvas() {
  const { executeEventAction, helpers } = useActionExecutor({
    onDeleteNode: async (nodeId) => await deleteNode({ nodeId, graphId }),
    onOpenContextMenu: (pos, nodeId) => openMenu(pos, nodeId),
    onCreateNode: async (pos) => await createNode(pos),
    onApplyForceLayout: async () => await applyLayout(),
  });

  const handleDoubleClick = (e: React.MouseEvent, nodeId: string) => {
    // Execute the action mapped to double-click
    executeEventAction(CanvasEventType.NodeDoubleClick, {
      eventType: CanvasEventType.NodeDoubleClick,
      nodeId,
      worldPosition: { x: 100, y: 100 },
      screenPosition: { x: e.clientX, y: e.clientY },
      modifiers: { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey },
    });
  };
}
```

### Register Custom Actions

```tsx
import { registerAction, ActionCategory } from '@blinksgg/canvas';

registerAction({
  id: 'my-custom-action',
  label: 'My Action',
  description: 'Does something custom',
  category: ActionCategory.Custom,
  icon: 'star',
  handler: async (context, helpers) => {
    // context has: eventType, nodeId, nodeData, worldPosition, screenPosition, modifiers
    // helpers has: selectNode, clearSelection, fitToBounds, lockNode, undo, redo, etc.
    if (context.nodeId) {
      helpers.selectNode(context.nodeId);
    }
  },
});
```

### Built-in Presets

| Preset | Description |
|--------|-------------|
| `default` | Standard interactions (current behavior) |
| `minimal` | Only selection and context menu |
| `power-user` | Quick actions (double-click lock, triple-click delete) |

### Wiring Node/Viewport Callbacks

```tsx
import { Node, Viewport, useActionExecutor, CanvasEventType } from '@blinksgg/canvas';

function MyCanvas() {
  const { executeEventAction, helpers } = useActionExecutor({
    onDeleteNode: (id) => deleteMutation.mutate({ nodeId: id, graphId }),
    onOpenContextMenu: (pos, nodeId) => setMenu({ pos, nodeId }),
  });

  return (
    <Viewport
      onBackgroundDoubleClick={(worldPos) => {
        executeEventAction(CanvasEventType.BackgroundDoubleClick, {
          eventType: CanvasEventType.BackgroundDoubleClick,
          worldPosition: worldPos,
          screenPosition: worldPos, // Approximate
          modifiers: { shift: false, ctrl: false, alt: false, meta: false },
        });
      }}
    >
      <NodeRenderer
        onRenderNode={(node) => (
          <Node
            nodeData={node}
            onDoubleClick={(id, data) => {
              executeEventAction(CanvasEventType.NodeDoubleClick, {
                eventType: CanvasEventType.NodeDoubleClick,
                nodeId: id,
                nodeData: data,
                worldPosition: { x: data.x, y: data.y },
                screenPosition: { x: 0, y: 0 },
                modifiers: { shift: false, ctrl: false, alt: false, meta: false },
              });
            }}
            onRightClick={(id, data, e) => {
              executeEventAction(CanvasEventType.NodeRightClick, {
                eventType: CanvasEventType.NodeRightClick,
                nodeId: id,
                worldPosition: { x: data.x, y: data.y },
                screenPosition: { x: e.clientX, y: e.clientY },
                modifiers: { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey },
              });
            }}
          />
        )}
      />
    </Viewport>
  );
}
```

---

## Settings Panel

Canvas provides a customizable settings system for mapping user interactions (events) to actions.

### useCanvasSettings Hook

```tsx
import { useCanvasSettings } from '@blinksgg/canvas';

function SettingsUI() {
  const {
    // Read-only state
    mappings,           // Current event-action mappings
    activePresetId,     // ID of active preset (null if modified)
    activePreset,       // Active preset object
    allPresets,         // All available presets (built-in + custom)
    hasUnsavedChanges,  // Whether mappings differ from preset
    isPanelOpen,        // Whether settings panel is visible

    // Actions
    setEventMapping,    // Set action for a specific event
    applyPreset,        // Apply a preset by ID
    saveAsPreset,       // Save current mappings as new preset
    updatePreset,       // Update existing preset with current mappings
    deletePreset,       // Delete a custom preset
    resetSettings,      // Reset to default settings
    togglePanel,        // Toggle settings panel visibility
    setPanelOpen,       // Set panel open/closed directly

    // Utilities
    getActionForEvent,  // Get action ID for an event
  } = useCanvasSettings();
}
```

### Settings Panel Component

```tsx
import { SettingsPanel, useCanvasSettings } from '@blinksgg/canvas';

function MyApp() {
  const { isPanelOpen, togglePanel } = useCanvasSettings();

  return (
    <>
      <button onClick={togglePanel}>⚙️ Settings</button>
      <SettingsPanel onClose={() => togglePanel()} />
    </>
  );
}
```

### Event Types

Events that can be mapped to actions:

| Event Type | Description |
|------------|-------------|
| `NodeDoubleClick` | Double-click on a node |
| `NodeTripleClick` | Triple-click on a node |
| `NodeRightClick` | Right-click on a node |
| `NodeLongPress` | Long-press on a node (touch) |
| `BackgroundClick` | Click on canvas background |
| `BackgroundDoubleClick` | Double-click on background |
| `BackgroundRightClick` | Right-click on background |
| `BackgroundLongPress` | Long-press on background (touch) |

### Built-in Actions

| Action ID | Description |
|-----------|-------------|
| `none` | No action (disabled) |
| `select-node` | Select the clicked node |
| `add-to-selection` | Add node to selection |
| `clear-selection` | Clear all selection |
| `delete-selected` | Delete selected nodes |
| `fit-to-view` | Fit node to viewport |
| `fit-all-to-view` | Fit entire graph to viewport |
| `center-on-node` | Center viewport on node |
| `reset-viewport` | Reset viewport to default |
| `lock-node` | Lock node (full-screen view) |
| `unlock-node` | Unlock node |
| `toggle-lock` | Toggle node lock state |
| `open-context-menu` | Open right-click menu |
| `apply-force-layout` | Apply force-directed layout |
| `undo` | Undo last action |
| `redo` | Redo last undone action |
| `create-node` | Create a new node |

### Built-in Presets

| Preset | Description |
|--------|-------------|
| `default` | Standard canvas behavior |
| `productivity` | Optimized for quick editing |
| `presentation` | Clean presentation mode |

### Custom Presets

```tsx
const { saveAsPreset, applyPreset, deletePreset } = useCanvasSettings();

// Save current mappings as a new preset
const newPresetId = saveAsPreset('My Custom Preset', 'Optional description');

// Apply a preset
applyPreset('productivity');
applyPreset(newPresetId);

// Delete a custom preset (built-in presets cannot be deleted)
deletePreset(newPresetId);
```

### Settings Atoms (for Jotai)

| Atom | Purpose |
|------|---------|
| `canvasSettingsAtom` | Full settings state (persisted) |
| `eventMappingsAtom` | Current event-action mappings |
| `activePresetIdAtom` | Currently active preset ID |
| `allPresetsAtom` | All presets (built-in + custom) |
| `isPanelOpenAtom` | Settings panel visibility |
| `hasUnsavedChangesAtom` | Whether mappings differ from preset |

### Action Atoms

| Atom | Purpose |
|------|---------|
| `setEventMappingAtom` | Set a single event-action mapping |
| `applyPresetAtom` | Apply a preset by ID |
| `saveAsPresetAtom` | Save current mappings as new preset |
| `updatePresetAtom` | Update existing preset |
| `deletePresetAtom` | Delete a custom preset |
| `resetSettingsAtom` | Reset to default settings |
| `togglePanelAtom` | Toggle settings panel |
| `setPanelOpenAtom` | Set panel open state |

---

## Command Console

An extensible command palette with fuzzy search, sequential input collection, and keyboard shortcuts.

### Import Path

```tsx
import { ... } from '@blinksgg/canvas/commands';
```

### Quick Start

```tsx
import {
  CommandProvider,
  CommandLine,
  CommandFeedbackOverlay,
  registerBuiltinCommands,
  registerCommand,
  useCommandLine,
} from '@blinksgg/canvas/commands';

// 1. Register built-in commands once
registerBuiltinCommands();

// 2. Register custom commands
registerCommand({
  name: 'createWidget',
  description: 'Create a new widget',
  category: 'custom',
  inputs: [{ type: 'text', name: 'name', prompt: 'Widget name' }],
  execute: async (inputs, ctx) => {
    await ctx.mutations.createNode({ type: 'widget', label: inputs.name });
  },
});

// 3. Wrap your app with CommandProvider
function App() {
  return (
    <CommandProvider
      onCreateNode={createNodeMutation.mutateAsync}
      onDeleteNode={deleteNodeMutation.mutateAsync}
    >
      <Canvas />
      <CommandLine />
      <CommandFeedbackOverlay />
    </CommandProvider>
  );
}
```

### Built-in Commands

| Command | Aliases | Description |
|---------|---------|-------------|
| `fitToView` | `fit`, `fitAll` | Fit all nodes in viewport |
| `fitSelection` | `fitSel` | Fit selected nodes in viewport |
| `resetViewport` | `reset`, `home` | Reset zoom and pan |
| `zoomIn` | `+`, `in` | Zoom in |
| `zoomOut` | `-`, `out` | Zoom out |
| `selectAll` | `all` | Select all nodes |
| `clearSelection` | `deselect`, `clear` | Clear selection |
| `invertSelection` | `invert` | Invert selection |
| `undo` | `z` | Undo last action |
| `redo` | `y` | Redo last action |
| `forceLayout` | `force`, `autoLayout` | Apply force-directed layout |

### Keyboard Shortcuts

| Shortcut | Action |
|----------|--------|
| `/` | Toggle command line |
| `Cmd+K` / `Ctrl+K` | Open command line |
| `ESC` | Cancel current command / return to search |
| `↑` / `↓` | Navigate suggestions |
| `Enter` | Select command / provide input |
| `Tab` | Autocomplete command name |

### useCommandLine Hook

```tsx
import { useCommandLine } from '@blinksgg/canvas/hooks';

function CommandToggle() {
  const {
    visible,      // Command line visibility
    state,        // Current state (searching, collecting, executing, error)
    history,      // Command history
    progress,     // Input collection progress { current, total }
    open,         // Open command line
    close,        // Close command line
    isSearching,  // Helper: state.phase === 'searching'
    isCollecting, // Helper: state.phase === 'collecting'
    isExecuting,  // Helper: state.phase === 'executing'
    hasError,     // Helper: state.phase === 'error'
  } = useCommandLine();

  return <button onClick={open}>Commands (⌘K)</button>;
}
```

### Register Custom Commands

```tsx
import { registerCommand, type CommandDefinition } from '@blinksgg/canvas/commands';

const myCommand: CommandDefinition = {
  name: 'myCommand',
  description: 'Does something cool',
  aliases: ['mc'],  // Optional shortcuts
  category: 'custom',  // 'nodes' | 'edges' | 'viewport' | 'selection' | 'history' | 'layout' | 'custom'
  inputs: [
    { type: 'text', name: 'title', prompt: 'Enter title' },
    { type: 'number', name: 'count', prompt: 'How many?', default: 1 },
    {
      type: 'select',
      name: 'color',
      prompt: 'Choose color',
      options: [
        { value: 'red', label: 'Red', shortcut: 'r' },
        { value: 'blue', label: 'Blue', shortcut: 'b' },
      ],
    },
    { type: 'point', name: 'position', prompt: 'Click location' },
    { type: 'node', name: 'target', prompt: 'Select target node' },
    { type: 'boolean', name: 'confirm', prompt: 'Are you sure?' },
  ],
  execute: async (inputs, ctx) => {
    // inputs: { title: string, count: number, color: string, position: {x,y}, target: string, confirm: boolean }
    // ctx: CommandContext with mutations, layout, history, viewport, selectedNodeIds
    await ctx.mutations.createNode({
      graph_id: ctx.currentGraphId,
      node_type: 'custom',
      label: inputs.title,
      ui_properties: { x: inputs.position.x, y: inputs.position.y },
    });
  },
};

registerCommand(myCommand);
```

### Input Types

| Type | Description | Value |
|------|-------------|-------|
| `text` | Text input | `string` |
| `number` | Numeric input | `number` |
| `boolean` | Yes/No choice | `boolean` |
| `select` | Dropdown options | `string` |
| `point` | Click on canvas | `{ x: number, y: number }` |
| `node` | Click on a node | `string` (nodeId) |
| `nodes` | Select multiple nodes | `string[]` (nodeIds) |

### CommandProvider Props

```tsx
interface CommandProviderProps {
  children: React.ReactNode;

  // Node mutations (app provides implementations)
  onCreateNode?: (payload: CreateNodePayload) => Promise<unknown>;
  onUpdateNode?: (nodeId: string, updates: Record<string, unknown>) => Promise<void>;
  onDeleteNode?: (nodeId: string) => Promise<void>;

  // Edge mutations
  onCreateEdge?: (payload: CreateEdgePayload) => Promise<unknown>;
  onDeleteEdge?: (edgeId: string) => Promise<void>;

  // Force layout persistence
  onForceLayoutPersist?: (positions: Array<{ nodeId: string; x: number; y: number }>) => Promise<void>;
}
```

### CommandContext (in execute function)

```tsx
interface CommandContext {
  get: JotaiStore['get'];           // Read atoms directly
  set: JotaiStore['set'];           // Write atoms directly
  currentGraphId: string | null;    // Active graph ID
  selectedNodeIds: Set<string>;     // Currently selected nodes
  viewport: { zoom: number; pan: { x: number; y: number } };

  mutations: {
    createNode: (payload: CreateNodePayload) => Promise<unknown>;
    updateNode: (nodeId: string, updates: Record<string, unknown>) => Promise<void>;
    deleteNode: (nodeId: string) => Promise<void>;
    createEdge: (payload: CreateEdgePayload) => Promise<unknown>;
    deleteEdge: (edgeId: string) => Promise<void>;
  };

  layout: {
    fitToBounds: (mode: 'graph' | 'selection', padding?: number) => void;
    applyForceLayout: () => Promise<void>;
  };

  history: {
    undo: () => void;
    redo: () => void;
  };
}
```

### Command Line UI Components

| Component | Purpose |
|-----------|---------|
| `CommandLine` | Main command bar (appears at bottom) |
| `CommandSearch` | Fuzzy search autocomplete |
| `CommandInputCollector` | Sequential input prompts |
| `CommandFeedbackOverlay` | Visual feedback (crosshairs, previews) |

### Store Atoms

| Atom | Purpose |
|------|---------|
| `commandLineVisibleAtom` | Command line visibility |
| `commandLineStateAtom` | Full state (phase, suggestions, etc.) |
| `commandFeedbackAtom` | Visual feedback state |
| `commandHistoryAtom` | Command execution history |
| `openCommandLineAtom` | Action: open command line |
| `closeCommandLineAtom` | Action: close command line |
| `selectCommandAtom` | Action: select a command |
| `isCommandActiveAtom` | Boolean: is command line active? |
| `currentInputAtom` | Current input being collected |
| `commandProgressAtom` | Progress: { current, total } |
