npm package: @immediate-diagram/react
Description: React component library for [Immediate Diagram](https://github.com/immediate-diagram/immediate-diagram) — an LLM-native diagramming platform that transforms natural-language prompts into rich, stateful, animated SVG diagrams.
Latest version: 1.2.3

# @immediate-diagram/react

React component library for [Immediate Diagram](https://github.com/immediate-diagram/immediate-diagram) — an LLM-native diagramming platform that transforms natural-language prompts into rich, stateful, animated SVG diagrams.

Render any of the 7 supported diagram types (flowchart, sequence, architecture, pie, donut, block, venn) from a concise text DSL — with theming, interactive zoom/pan, timeline playback, and accessibility built in.

## Installation

```bash
npm install @immediate-diagram/react
# or
bun add @immediate-diagram/react
# or
yarn add @immediate-diagram/react
# or
pnpm add @immediate-diagram/react
```

**Peer dependencies:** React ≥ 18.0.0 and ReactDOM ≥ 18.0.0 must be installed in your project.

## Quick Start

Render a diagram in 3 lines:

```tsx
import { ImmediateDiagram } from "@immediate-diagram/react";

function App() {
  return <ImmediateDiagram dsl="flowchart\n  A --> B --> C" />;
}
```

That's it — parsing, layout, rendering, and theming are handled automatically.

---

## Table of Contents

- [Components](#components)
  - [ImmediateDiagram](#immediatediagram) — All-in-one (recommended)
  - [DiagramViewer](#diagramviewer) — From parsed AST
  - [DiagramViewerFromSource](#diagramviewerfromsource) — From DSL string
  - [InteractiveDiagramViewer](#interactivediagramviewer) — Zoom/pan/collapse
  - [PlaybackController](#playbackcontroller) — Timeline transport bar
  - [ZoomControls](#zoomcontrols) — Floating zoom buttons
  - [CopyImageButton](#copyimagebutton) — Copy diagram as PNG
  - [DiagramTooltip](#diagramtooltip) — Element tooltips
  - [ThemeToggle](#themetoggle) — Light/dark toggle
  - [Toast & ToastContainer](#toast--toastcontainer) — Notifications
  - [ConfirmDialog](#confirmdialog) — Confirmation modal
  - [DiagramWrapper](#diagramwrapper) — Theme-aware container
  - [DiagramErrorBoundary](#diagramerrorboundary) — Error boundary
  - [RecoveryErrorPanel](#recoveryerrorpanel) — Parse error display
  - [LoadingSpinner](#loadingspinner) — Loading indicator
  - [Button](#button) — Styled button
- [Context & Provider](#context--provider)
  - [DiagramProvider](#diagramprovider)
  - [useDiagramContext](#usediagramcontext)
  - [Convenience Hooks](#convenience-hooks)
- [Hooks](#hooks)
  - [Core Hooks](#core-hooks)
  - [Viewport Hooks](#viewport-hooks)
  - [Utility Hooks](#utility-hooks)
- [Utilities](#utilities)
- [Theming](#theming)
- [Examples](#examples)
  - [Basic Rendering](#basic-rendering)
  - [Dark Theme](#dark-theme)
  - [Interactive Viewer with Zoom/Pan](#interactive-viewer-with-zoomopan)
  - [Timeline Playback](#timeline-playback)
  - [Custom Viewer with Hooks](#custom-viewer-with-hooks)
  - [Multiple Diagrams on One Page](#multiple-diagrams-on-one-page)
  - [Error Handling](#error-handling)
  - [Lazy Loading](#lazy-loading)
  - [Copy to Clipboard](#copy-to-clipboard)
- [CSS (Optional)](#css-optional)
- [Migration from @immediate-diagram/web](#migration-from-immediate-diagramweb)
- [TypeScript Support](#typescript-support)
- [Browser Support](#browser-support)
- [Troubleshooting](#troubleshooting)
- [API Reference](#api-reference)

---

## Components

### ImmediateDiagram

The primary, all-in-one integration component. Takes raw DSL text and handles everything: parsing, layout, SVG rendering, theming, error display, and optional timeline playback.

**This is the recommended starting point for most use cases.**

```tsx
import { ImmediateDiagram } from "@immediate-diagram/react";

<ImmediateDiagram
  dsl={`flowchart
  Start --> Process --> End`}
  theme="dark"
  interactive
  showControls
  onStateChange={(state) => console.log("State:", state)}
/>
```

#### Props — `ImmediateDiagramProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `dsl` | `string` | **(required)** | Raw IMD DSL source string to parse and render. |
| `theme` | `"light" \| "dark"` | `"light"` | Theme for the diagram and playback controller. |
| `width` | `number \| string` | `"100%"` | Container width (number = px, string = CSS value). |
| `height` | `number \| string` | `"auto"` | Container height (number = px, string = CSS value). |
| `animate` | `boolean` | `true` | Enable CSS transitions for state changes. |
| `showControls` | `boolean` | `undefined` | Show playback controls. `undefined` = auto-detect (show if ≥ 2 states). |
| `autoPlay` | `boolean` | `false` | Auto-play the timeline on mount. |
| `defaultTimeline` | `string` | `undefined` | Name of the initial timeline to activate. |
| `playbackSpeed` | `number` | `1` | Initial playback speed multiplier. |
| `interactive` | `boolean` | `false` | Enable zoom/pan viewport (mouse wheel, drag, pinch). |
| `showZoomControls` | `boolean` | `false` | Show floating zoom control overlay (requires `interactive`). |
| `initialZoom` | `"fit" \| "actual"` | `"fit"` | Initial zoom: fit to container or 100% (requires `interactive`). |
| `minScale` | `number` | `0.1` | Minimum zoom scale (requires `interactive`). |
| `maxScale` | `number` | `10` | Maximum zoom scale (requires `interactive`). |
| `sanitize` | `boolean` | `false` | Sanitize SVG through DOMPurify before rendering. |
| `className` | `string` | — | Additional CSS class for the container. |
| `style` | `CSSProperties` | — | Additional inline styles for the container. |
| `onStateChange` | `(state: string) => void` | — | Called when the diagram transitions to a new state. |
| `onNodeClick` | `(nodeId: string) => void` | — | Called when a diagram node is clicked. |
| `onTimelineEnd` | `() => void` | — | Called when the timeline reaches the end. |
| `onError` | `(error, errorInfo) => void` | — | Error callback for rendering failures. |
| `errorFallback` | `(error, reset) => ReactNode` | — | Custom render error fallback. |
| `renderError` | `(error, source) => ReactNode` | — | Custom parse error renderer. |
| `data-testid` | `string` | `"immediate-diagram"` | Test ID for the container element. |

#### Imperative Ref — `ImmediateDiagramRef`

For programmatic playback control, use a ref:

```tsx
import { useRef } from "react";
import { ImmediateDiagram, type ImmediateDiagramRef } from "@immediate-diagram/react";

function App() {
  const ref = useRef<ImmediateDiagramRef>(null);

  return (
    <>
      <ImmediateDiagram ref={ref} dsl={dsl} showControls />
      <button onClick={() => ref.current?.play()}>Play</button>
      <button onClick={() => ref.current?.goToState("peak")}>Go to "peak"</button>
    </>
  );
}
```

| Method | Description |
|--------|-------------|
| `play()` | Start or resume playback. |
| `pause()` | Pause playback. |
| `stepForward()` | Advance to the next state. |
| `stepBack()` | Go back to the previous state. |
| `goToState(name)` | Jump to a named state. |
| `setSpeed(speed)` | Set playback speed multiplier. |
| `setTimeline(name)` | Switch to a different timeline. |
| `getCurrentState()` | Get the current state name. |
| `getTimelines()` | Get all available timeline names. |

#### Playback Controller Behavior

The playback controller adapts to the number of states:

- **0–1 states**: No controller rendered.
- **2 states**: Compact toggle button.
- **3+ states**: Full controller with scrubber, speed, and loop controls.

---

### DiagramViewer

Lower-level component that accepts a pre-parsed `DiagramAST`. Must be wrapped in a `DiagramProvider`.

Use this when you already have a parsed AST, need fine-grained control over layout/render options, or want to share a single `DiagramProvider` across multiple diagrams.

```tsx
import { parse } from "@immediate-diagram/parser";
import { DiagramProvider, DiagramViewer } from "@immediate-diagram/react";

const ast = parse("flowchart\n  A --> B");

function App() {
  return (
    <DiagramProvider>
      <DiagramViewer ast={ast} />
    </DiagramProvider>
  );
}
```

#### Props — `DiagramViewerProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ast` | `DiagramAST` | **(required)** | Parsed diagram AST from the parser. |
| `stableViewBox` | `BoundingBox` | — | Fixed viewBox for state transitions (prevents layout shift). |
| `debounceMs` | `number` | — | Debounce delay for AST changes (ms). 100–300 recommended for live editing. |
| `lazy` | `boolean` | `false` | Enable IntersectionObserver-based lazy rendering. |
| `lazyRootMargin` | `string` | `"0px"` | Root margin for lazy IntersectionObserver. |
| `sanitize` | `boolean` | `false` | Sanitize SVG through DOMPurify. |
| `tooltip` | `boolean` | `false` | Enable interactive tooltips on diagram elements. |
| `tooltipSelector` | `string` | `".imd-arc, .imd-circle, .imd-node-group"` | CSS selector for tooltip-triggering elements. |
| `layoutOptions` | `object` | — | Override layout engine defaults (`maxWidth`, `padding`, `nodeSpacing`, etc.). |
| `renderOptions` | `Omit<RenderOptions, "theme">` | — | Override SVG render options (theme is always set from context). |
| `className` | `string` | — | Additional CSS class. |
| `style` | `CSSProperties` | — | Additional inline styles. |
| `pageTheme` | `"light" \| "dark"` | — | Page-level theme (auto-detected from `document` if omitted). |
| `borderRadius` | `number` | `8` | Wrapper border radius (px). |
| `padding` | `number` | `16` | Wrapper padding (px). |
| `skeletonWidth` | `number` | `600` | Skeleton placeholder width (px). |
| `skeletonHeight` | `number` | `400` | Skeleton placeholder height (px). |
| `skeletonVariant` | `"shimmer" \| "pulse" \| "none"` | auto | Skeleton animation variant. |
| `onError` | `(error, errorInfo) => void` | — | Error boundary callback. |
| `errorFallback` | `(error, reset) => ReactNode` | — | Custom error fallback. |
| `aria-label` | `string` | `"Diagram"` | Accessible label. |
| `data-testid` | `string` | `"diagram-viewer"` | Test ID. |

---

### DiagramViewerFromSource

Mid-level component that accepts a DSL string, parses it with error recovery, and renders. Must be wrapped in a `DiagramProvider`.

Useful when you want the parse-and-render pipeline but need to control the provider separately (e.g., shared font loading, custom theme).

```tsx
import { DiagramProvider, DiagramViewerFromSource } from "@immediate-diagram/react";

function Editor({ source }: { source: string }) {
  return (
    <DiagramProvider theme="dark">
      <DiagramViewerFromSource source={source} debounceMs={200} />
    </DiagramProvider>
  );
}
```

#### Props — `DiagramViewerFromSourceProps`

Extends most `DiagramViewerProps` (except `ast`), plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `source` | `string` | **(required)** | Raw IMD DSL source string. |
| `debounceMs` | `number` | — | Debounce delay for source changes (ms). |
| `renderParseError` | `(errors, source, hasPartial) => ReactNode` | — | Custom parse error renderer. |

When parse errors occur, the component shows both the partial diagram (if recoverable) and a `RecoveryErrorPanel` with all errors and suggestions.

---

### InteractiveDiagramViewer

Full interactive viewer with zoom/pan. Extends `DiagramViewer` with:

- **Mouse wheel zoom** (cursor-centered)
- **Click-drag pan**
- **Double-click fit-to-view**
- **Touch pinch-zoom** on mobile
- **Keyboard shortcuts** (arrow keys for pan, +/- for zoom, 0 to reset)
- **Block collapse/expand** for block diagrams (click block labels)
- **Optional zoom controls overlay**
- **Optional copy-to-clipboard button**

Must be wrapped in a `DiagramProvider`.

```tsx
import { DiagramProvider, InteractiveDiagramViewer } from "@immediate-diagram/react";

<DiagramProvider>
  <InteractiveDiagramViewer
    ast={ast}
    interactive
    initialZoom="fit"
    showZoomControls
    showCopyButton
    minScale={0.1}
    maxScale={5}
  />
</DiagramProvider>
```

#### Props — `InteractiveDiagramViewerProps`

Extends all `DiagramViewerProps`, plus:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `interactive` | `boolean` | `true` | Enable zoom/pan interactivity. |
| `initialZoom` | `"fit" \| "actual"` | `"fit"` | Initial zoom behavior. |
| `showZoomControls` | `boolean` | `false` | Show floating zoom control overlay. |
| `showCopyButton` | `boolean` | `false` | Show "Copy as image" button overlay. |
| `minScale` | `number` | `0.1` | Minimum zoom scale. |
| `maxScale` | `number` | `10` | Maximum zoom scale. |
| `onCopySuccess` | `() => void` | — | Called after successful clipboard copy. |
| `onFallbackDownload` | `() => void` | — | Called when clipboard write falls back to download. |
| `onCopyError` | `(message: string) => void` | — | Called when copy/download fails. |

#### Block Collapse

For block diagrams, clicking a block label toggles collapse/expand. Collapsed blocks display as compact placeholder rectangles with a ▶ indicator and child count badge. This is implemented as a pure AST transformation before layout — the layout engine always receives a clean AST.

---

### PlaybackController

Standalone playback transport bar for timeline-based diagrams. Wire it to the `usePlaybackController` hook.

```tsx
import { PlaybackController, usePlaybackController, DiagramProvider } from "@immediate-diagram/react";

function TimelineViewer({ ast }) {
  const controller = usePlaybackController({
    timelines: ast.timelines,
    transitions: ast.transitions,
  });

  return (
    <DiagramProvider>
      <DiagramViewer ast={applyState(ast, controller.currentStateName)} />
      <PlaybackController controller={controller} theme="dark" />
    </DiagramProvider>
  );
}
```

#### Props — `PlaybackControllerProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `controller` | `PlaybackControllerState` | **(required)** | State object from `usePlaybackController()`. |
| `theme` | `"light" \| "dark"` | `"light"` | Theme for the controller UI. |
| `className` | `string` | — | Additional CSS class. |
| `keyboardEnabled` | `boolean` | `true` | Enable keyboard shortcuts (Space = play/pause, ←/→ = step). |

---

### ZoomControls

Floating zoom control buttons for interactive viewers. Includes zoom in, zoom out, fit-to-view, and reset (1:1) buttons with a scale percentage display.

```tsx
<ZoomControls
  onZoomIn={zoomIn}
  onZoomOut={zoomOut}
  onFitToView={fitToView}
  onResetZoom={resetZoom}
  currentScale={1.0}
  position="bottom-right"
  theme="light"
/>
```

#### Props — `ZoomControlsProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `onZoomIn` | `() => void` | **(required)** | Zoom in callback. |
| `onZoomOut` | `() => void` | **(required)** | Zoom out callback. |
| `onFitToView` | `() => void` | **(required)** | Fit-to-view callback. |
| `onResetZoom` | `() => void` | **(required)** | Reset to 1:1 callback. |
| `currentScale` | `number` | **(required)** | Current scale (1.0 = 100%). |
| `theme` | `"light" \| "dark"` | `"light"` | Theme. |
| `position` | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right"` | `"bottom-right"` | Corner position. |
| `className` | `string` | — | Additional CSS class. |

---

### CopyImageButton

Floating button to copy the rendered diagram as a PNG image to the clipboard. Falls back to file download when the Clipboard API is unavailable.

```tsx
const containerRef = useRef<HTMLDivElement>(null);

<div ref={containerRef}>
  <DiagramViewer ast={ast} />
  <CopyImageButton containerRef={containerRef} position="top-right" />
</div>
```

#### Props — `CopyImageButtonProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `containerRef` | `RefObject<HTMLElement>` | **(required)** | Ref to the SVG container. |
| `theme` | `"light" \| "dark"` | `"light"` | Theme. |
| `position` | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right"` | `"top-right"` | Corner position. |
| `pixelRatio` | `number` | `2` | Pixel ratio for rasterization (2 = Retina). |
| `backgroundColor` | `string` | `"transparent"` | Background color of the rasterized image. |
| `padding` | `number` | `16` | Padding around the diagram in the image (px). |
| `downloadFilename` | `string` | `"diagram"` | Filename for fallback download (without extension). |
| `onSuccess` | `() => void` | — | Called after successful copy. |
| `onFallbackDownload` | `() => void` | — | Called on fallback to file download. |
| `onError` | `(message: string) => void` | — | Called on error. |
| `className` | `string` | — | Additional CSS class. |

---

### DiagramTooltip

Interactive tooltip wrapper for diagram elements. Shows rich, cursor-tracking tooltips on hover/touch for arcs (pie/donut), circles (venn), and nodes (graph).

```tsx
<DiagramTooltip theme="dark" enabled selector=".imd-arc, .imd-circle">
  <div dangerouslySetInnerHTML={{ __html: svgString }} />
</DiagramTooltip>
```

#### Props — `DiagramTooltipProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | **(required)** | Content to wrap. |
| `theme` | `"light" \| "dark"` | `"light"` | Tooltip theme. |
| `enabled` | `boolean` | `true` | Whether tooltips are enabled. |
| `selector` | `string` | `".imd-arc, .imd-circle, .imd-node-group"` | CSS selector for tooltip-triggering elements. |

The tooltip reads `data-label`, `data-value`, `data-percentage`, `data-node-id`, `data-set-id`, and `data-id` attributes from the matched SVG elements.

---

### ThemeToggle

Light/dark theme toggle button with animated sun/moon icons.

```tsx
import { ThemeToggle, useThemePreference } from "@immediate-diagram/react";

function Header() {
  const { theme, toggleTheme } = useThemePreference();
  return <ThemeToggle theme={theme} onToggle={toggleTheme} />;
}
```

#### Props — `ThemeToggleProps`

| Prop | Type | Description |
|------|------|-------------|
| `theme` | `"light" \| "dark"` | Currently active theme. |
| `onToggle` | `() => void` | Toggle callback. |

> **Note:** Requires `@immediate-diagram/react/styles.css` for full visual styling.

---

### Toast & ToastContainer

Notification system with auto-dismiss. Use the `useToast` hook for state management.

```tsx
import { ToastContainer, useToast } from "@immediate-diagram/react";
import "@immediate-diagram/react/styles.css";

function App() {
  const { toasts, addToast } = useToast();

  return (
    <>
      <button onClick={() => addToast("Diagram copied!", "success")}>
        Copy
      </button>
      <ToastContainer toasts={toasts} onDismiss={(id) => {/* handled by hook */}} />
    </>
  );
}
```

#### `ToastMessage`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique toast ID. |
| `message` | `string` | Message text. |
| `variant` | `"success" \| "error" \| "info"` | Visual variant (green/red/blue). |

#### `ToastContainerProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `toasts` | `ToastMessage[]` | **(required)** | Active toasts. |
| `onDismiss` | `(id: string) => void` | **(required)** | Dismiss callback. |
| `duration` | `number` | `3000` | Auto-dismiss duration (ms). |

---

### ConfirmDialog

Accessible confirmation modal dialog.

```tsx
<ConfirmDialog
  open={isOpen}
  title="Delete Diagram"
  message="Are you sure? This action cannot be undone."
  confirmLabel="Delete"
  confirmVariant="danger"
  onConfirm={handleDelete}
  onCancel={() => setIsOpen(false)}
/>
```

#### Props — `ConfirmDialogProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `open` | `boolean` | **(required)** | Whether the dialog is visible. |
| `title` | `string` | **(required)** | Dialog title. |
| `message` | `string` | **(required)** | Dialog message. |
| `confirmLabel` | `string` | `"Confirm"` | Confirm button label. |
| `confirmVariant` | `"danger" \| "primary"` | `"primary"` | Confirm button variant. |
| `onConfirm` | `() => void` | **(required)** | Confirm callback. |
| `onCancel` | `() => void` | **(required)** | Cancel callback. |

---

### DiagramWrapper

Theme-aware container that provides background styling, border, and visual boundary detection. Used internally by `DiagramViewer` but can be used standalone for custom compositions.

When the page theme and diagram theme differ, a subtle visual boundary (border) is automatically added to make the contrast feel intentional.

```tsx
<DiagramWrapper metadata={ast.metadata} pageTheme="dark" padding={24}>
  {/* your diagram content */}
</DiagramWrapper>
```

#### Props — `DiagramWrapperProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `metadata` | `DiagramMetadata \| null` | — | AST metadata for theme resolution. |
| `children` | `ReactNode` | **(required)** | Content. |
| `className` | `string` | — | Additional CSS class. |
| `style` | `CSSProperties` | — | Additional styles. |
| `pageTheme` | `"light" \| "dark"` | auto | Page-level theme (auto-detected from DOM). |
| `borderRadius` | `number` | `8` | Border radius (px). |
| `padding` | `number` | `16` | Padding (px). |
| `aria-label` | `string` | — | Accessible label. |
| `data-testid` | `string` | — | Test ID. |

---

### DiagramErrorBoundary

React error boundary for diagram rendering. Catches errors from layout, rendering, or SVG injection and displays a fallback UI with a retry button.

```tsx
<DiagramErrorBoundary
  onError={(error, info) => logToSentry(error, info)}
  fallback={(error, reset) => (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )}
>
  <DiagramViewer ast={ast} />
</DiagramErrorBoundary>
```

#### Props — `DiagramErrorBoundaryProps`

| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Components to wrap. |
| `onError` | `(error, errorInfo) => void` | Error callback (for logging/telemetry). |
| `fallback` | `(error, reset) => ReactNode` | Custom fallback UI. |

---

### RecoveryErrorPanel

Displays multiple parse errors with line numbers, source context, and "Did you mean?" suggestions. Used internally by `DiagramViewerFromSource` and `ImmediateDiagram`.

```tsx
<RecoveryErrorPanel
  errors={parseResult.errors}
  source={dslSource}
  hasPartialDiagram={parseResult.ast !== null}
/>
```

#### Props — `RecoveryErrorPanelProps`

| Prop | Type | Description |
|------|------|-------------|
| `errors` | `RecoveryParseError[]` | Parse errors to display. |
| `source` | `string` | Original DSL source for context. |
| `hasPartialDiagram` | `boolean` | Whether partial AST was recovered. |
| `data-testid` | `string` | Test ID (default: `"recovery-error-panel"`). |

---

### LoadingSpinner

Branded loading indicator with animated spinner ring and customizable message.

```tsx
<LoadingSpinner message="Rendering diagram…" />
```

#### Props — `LoadingSpinnerProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `message` | `string` | `"Loading…"` | Message below the spinner. |
| `data-testid` | `string` | `"loading-spinner"` | Test ID. |

> **Note:** Requires `@immediate-diagram/react/styles.css` for the animation.

---

### Button

Generic styled button with variant support.

```tsx
<Button variant="primary" onClick={handleSave}>Save</Button>
<Button variant="danger" onClick={handleDelete} disabled>Delete</Button>
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | **(required)** | Button content. |
| `onClick` | `() => void` | — | Click handler. |
| `disabled` | `boolean` | `false` | Disabled state. |
| `variant` | `"primary" \| "secondary" \| "danger"` | `"primary"` | Visual variant. |

---

## Context & Provider

### DiagramProvider

Provides the layout engine, text measurement (Canvas-based with font fallback), and theme configuration to all diagram components in the tree.

**Required for:** `DiagramViewer`, `DiagramViewerFromSource`, `InteractiveDiagramViewer`.

**Not required for:** `ImmediateDiagram` (creates its own provider internally).

```tsx
import { DiagramProvider, DiagramViewer } from "@immediate-diagram/react";

function App() {
  return (
    <DiagramProvider theme="dark" fontFamily="Fira Code, monospace">
      <DiagramViewer ast={ast1} />
      <DiagramViewer ast={ast2} />
    </DiagramProvider>
  );
}
```

#### Props — `DiagramProviderProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `fontFamily` | `string` | `"Inter, system-ui, Avenir, Helvetica, Arial, sans-serif"` | CSS font-family for text measurement. |
| `layoutEngine` | `LayoutEngine` | auto | Pre-configured layout engine (auto-created if omitted). |
| `theme` | `"light" \| "dark" \| Partial<DiagramTheme>` | `"light"` | Theme preset or partial theme override. |
| `children` | `ReactNode` | **(required)** | Child components. |

#### Theme Priority

Theme resolution follows this priority:

1. **`DiagramProvider` `theme` prop** — highest priority (programmatic override)
2. **DSL `theme dark` directive** — from the diagram source
3. **Default light theme** — lowest priority

When `DiagramProvider` has an explicit `theme` prop, it overrides all DSL `theme` directives. When omitted, individual diagrams can control their own theme via the DSL.

### useDiagramContext

Access the full diagram context value:

```tsx
const { measureText, fontsReady, layoutEngine, theme, hasExplicitTheme } = useDiagramContext();
```

Returns a `DiagramContextValue` with:

| Field | Type | Description |
|-------|------|-------------|
| `measureText` | `MeasureTextFn` | Text measurement function (Canvas-backed or heuristic). |
| `fontsReady` | `boolean` | Whether web fonts have loaded (measurement is accurate). |
| `layoutEngine` | `LayoutEngine` | Shared layout engine with all strategies. |
| `theme` | `DiagramTheme` | Current theme configuration. |
| `hasExplicitTheme` | `boolean` | Whether the provider has a programmatic theme set. |

### Convenience Hooks

These hooks are shortcuts for accessing specific context values:

| Hook | Returns | Description |
|------|---------|-------------|
| `useEffectiveTheme(metadata)` | `DiagramTheme` | Resolves theme considering provider + DSL `theme` directive. |
| `useDiagramTheme()` | `DiagramTheme` | Returns the provider's theme directly. |
| `useFontsReady()` | `boolean` | Whether web fonts are loaded. |
| `useLayoutEngine()` | `LayoutEngine` | The shared layout engine. |
| `useMeasureText()` | `MeasureTextFn` | The text measurement function. |

#### DiagramSkeleton

Loading placeholder shown while fonts are initializing:

```tsx
import { DiagramSkeleton } from "@immediate-diagram/react";

<DiagramSkeleton width={600} height={400} variant="shimmer" />
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `width` | `number` | `600` | Width (px). |
| `height` | `number` | `400` | Height (px). |
| `variant` | `"shimmer" \| "pulse" \| "none"` | auto | Animation variant (auto-detects `prefers-reduced-motion`). |
| `className` | `string` | — | Additional CSS class. |

---

## Hooks

### Core Hooks

#### `usePlaybackController(options)`

Headless state machine for timeline playback. Powers the `PlaybackController` component.

```tsx
const controller = usePlaybackController({
  timelines: ast.timelines,
  transitions: ast.transitions,
});

// controller.play(), controller.pause(), controller.stepForward(), ...
// controller.currentStateName, controller.stepIndex, controller.totalSteps, ...
```

**Options — `UsePlaybackControllerOptions`:**

| Option | Type | Description |
|--------|------|-------------|
| `timelines` | `TimelineBlock[]` | Timeline blocks from the parsed AST. |
| `transitions` | `TransitionBlock[]` | Transition blocks (for default inference). |
| `managerOptions` | `object` | Custom TimelineManager options. |

**Returns — `PlaybackControllerState`:**

| Field | Type | Description |
|-------|------|-------------|
| `playbackState` | `PlaybackState` | Current state (idle, playing, paused). |
| `stepIndex` | `number` | Current step (0-based). |
| `totalSteps` | `number` | Total steps in the active timeline. |
| `stateLabels` | `string[]` | Ordered state names. |
| `currentStateName` | `string` | Current state name. |
| `loop` | `boolean` | Whether looping is enabled. |
| `speed` | `SpeedOption` | Current speed (0.5, 1, or 2). |
| `activeTimelineName` | `string \| null` | Active timeline name. |
| `timelineNames` | `string[]` | All available timelines. |
| `hasTimelines` | `boolean` | Whether any timelines exist. |
| `play` | `() => void` | Start/resume playback. |
| `pause` | `() => void` | Pause playback. |
| `togglePlayPause` | `() => void` | Toggle play/pause. |
| `stepForward` | `() => void` | Go to next state. |
| `stepBack` | `() => void` | Go to previous state. |
| `goToStep` | `(index: number) => void` | Jump to a specific step. |
| `toggleLoop` | `() => void` | Toggle looping. |
| `setSpeed` | `(speed: SpeedOption) => void` | Set speed multiplier. |
| `setTimeline` | `(name: string) => void` | Switch timeline. |

---

#### `useRecoveryParse(source, maxErrors?)`

Parses DSL source with error recovery — collects ALL parse errors (not just the first) and produces a partial AST when possible.

```tsx
const { ast, errors, isComplete, skippedLines } = useRecoveryParse(dslSource);

if (ast) {
  // Render the (possibly partial) diagram
}
if (errors.length > 0) {
  // Show errors
}
```

**Returns — `RecoveryParseState`:**

| Field | Type | Description |
|-------|------|-------------|
| `ast` | `DiagramAST \| null` | Narrowed AST (null if header failed). |
| `errors` | `RecoveryParseError[]` | All collected parse errors. |
| `isComplete` | `boolean` | Whether parsing succeeded without errors. |
| `skippedLines` | `number[]` | Line numbers skipped during recovery. |

---

#### `useTimelinePlayback(diagram)`

Lower-level timeline hook wrapping the headless `TimelineManager` for React.

```tsx
const {
  timelineNames, activeTimeline, hasTimelines,
  playbackState, currentState,
  play, pause, stepForward, stepBack, goToState,
} = useTimelinePlayback(diagram);
```

---

#### `useBlockCollapseState(allBlockIds)`

Manages collapse/expand state for block diagram nodes.

```tsx
import { useBlockCollapseState, collectAllBlockIds } from "@immediate-diagram/react";

const blockIds = collectAllBlockIds(blockAst);
const { collapsedBlocks, toggleBlock, expandAll, collapseAll, isCollapsed } =
  useBlockCollapseState(blockIds);
```

**Returns — `UseBlockCollapseStateReturn`:**

| Field | Type | Description |
|-------|------|-------------|
| `collapsedBlocks` | `Set<string>` | Currently collapsed block IDs. |
| `toggleBlock` | `(id: string) => void` | Toggle a block. |
| `expandAll` | `() => void` | Expand all blocks. |
| `collapseAll` | `() => void` | Collapse all blocks. |
| `isCollapsed` | `(id: string) => boolean` | Check if a block is collapsed. |

---

### Viewport Hooks

#### `useViewportTransform(options)`

Manages zoom/pan state for interactive diagram viewers. Uses direct DOM manipulation for 60fps performance during continuous interactions.

```tsx
const {
  containerRef, viewportState,
  zoomIn, zoomOut, zoomTo, fitToView, resetZoom,
  panBy, resetPan,
} = useViewportTransform({
  svgWidth: 800,
  svgHeight: 600,
  minScale: 0.1,
  maxScale: 10,
});

return <div ref={containerRef}>{/* SVG content */}</div>;
```

**Key options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `svgWidth` | `number` | **(required)** | SVG content width (from viewBox). |
| `svgHeight` | `number` | **(required)** | SVG content height (from viewBox). |
| `minScale` | `number` | `0.1` | Minimum zoom scale. |
| `maxScale` | `number` | `10` | Maximum zoom scale. |
| `zoomStep` | `number` | `0.1` | Scale delta per wheel tick. |
| `enabled` | `boolean` | `true` | Enable/disable interactions. |

**Returns — `UseViewportTransformReturn`:**

| Field | Type | Description |
|-------|------|-------------|
| `containerRef` | `RefObject<HTMLDivElement>` | Attach to the SVG container. |
| `viewportState` | `ViewportState` | Current `{ scale, translateX, translateY }`. |
| `zoomIn` / `zoomOut` | `() => void` | Zoom step functions. |
| `zoomTo` | `(scale: number) => void` | Zoom to specific scale. |
| `fitToView` | `() => void` | Fit diagram to container. |
| `resetZoom` | `() => void` | Reset to 1:1 centered. |
| `panBy` | `(dx, dy) => void` | Pan by pixel delta. |
| `resetPan` | `() => void` | Reset pan to origin. |
| `applyTransformToDOM` | `() => void` | Apply current state to DOM (for gesture hooks). |
| `syncToReactState` | `() => void` | Sync DOM state to React (call on gesture end). |

---

#### `usePinchZoom(options)`

Multi-touch gesture support for mobile/tablet. Handles single-finger drag (pan) and two-finger pinch (zoom).

```tsx
usePinchZoom({
  containerRef,
  viewportStateRef: transform.viewportStateRef,
  applyTransform: transform.applyTransformToDOM,
  minScale: 0.1,
  maxScale: 10,
  onGestureEnd: transform.syncToReactState,
});
```

This hook is used internally by `InteractiveDiagramViewer` and typically doesn't need to be used directly.

---

#### `useViewportKeyboard(options)`

Keyboard shortcuts for diagram viewport navigation:

| Key | Action |
|-----|--------|
| `+` / `=` | Zoom in |
| `-` | Zoom out |
| `0` | Reset zoom to 100% |
| `Arrow keys` | Pan (50px step) |
| `Shift + Arrow` | Pan fast (200px step) |
| `Home` | Fit to view |
| `End` | Reset pan |

Used internally by `InteractiveDiagramViewer`.

---

### Utility Hooks

#### `useDebouncedValue(value, delayMs?)`

Debounces a value change. Returns the last stable value after `delayMs` of inactivity.

```tsx
const debouncedSource = useDebouncedValue(liveSource, 200);
// debouncedSource updates 200ms after the last liveSource change
```

---

#### `useThemePreference()`

Manages light/dark theme preference with localStorage persistence and system preference detection.

```tsx
const { theme, toggleTheme, setTheme, isUserSet } = useThemePreference();
```

**Returns — `ThemePreference`:**

| Field | Type | Description |
|-------|------|-------------|
| `theme` | `"light" \| "dark"` | Active theme. |
| `toggleTheme` | `() => void` | Toggle and persist. |
| `setTheme` | `(theme) => void` | Set explicitly and persist. |
| `isUserSet` | `boolean` | Whether the user explicitly chose (vs. system default). |

---

#### `usePageTheme()`

Reactively tracks the page-level `data-theme` attribute on `<html>` via a shared singleton `MutationObserver`.

```tsx
const pageTheme = usePageTheme(); // "light" | "dark"
```

---

#### `useReducedMotion()`

Detects `prefers-reduced-motion: reduce` media query. Re-renders on preference change.

```tsx
const prefersReducedMotion = useReducedMotion();
// true when the user has enabled reduced motion in OS settings
```

---

#### `useLazyRender(ref, options?)`

IntersectionObserver-based lazy rendering. The target element renders a placeholder until it scrolls into view.

```tsx
const containerRef = useRef<HTMLDivElement>(null);
const { hasBeenVisible } = useLazyRender(containerRef, { rootMargin: "100px" });

return (
  <div ref={containerRef}>
    {hasBeenVisible ? <DiagramViewer ast={ast} /> : <DiagramSkeleton />}
  </div>
);
```

**Returns — `UseLazyRenderReturn`:**

| Field | Type | Description |
|-------|------|-------------|
| `hasBeenVisible` | `boolean` | Latching — once `true`, stays `true`. |
| `isIntersecting` | `boolean` | Currently in viewport (dynamic). |

---

#### `useCopyDiagramAsImage(options)`

Copy a rendered SVG diagram to clipboard as PNG.

```tsx
const { copyAsImage, status, errorMessage } = useCopyDiagramAsImage({
  containerRef: diagramRef,
  pixelRatio: 2,
  backgroundColor: "#ffffff",
});
```

**Status values:** `"idle" | "copying" | "success" | "fallback-download" | "error"`

---

#### `useSplitPane(options?)`

Resizable split-pane layout for editor/preview layouts.

```tsx
const { ratio, isDragging, handlePointerDown, setRatio } = useSplitPane({
  initialRatio: 0.5,
  minRatio: 0.2,
  maxRatio: 0.8,
});

return (
  <div style={{ display: "flex" }}>
    <div style={{ width: `${ratio * 100}%` }}>Editor</div>
    <div onPointerDown={handlePointerDown} style={{ cursor: "col-resize", width: 8 }} />
    <div style={{ width: `${(1 - ratio) * 100}%` }}>Preview</div>
  </div>
);
```

---

## Utilities

Exported utility functions for advanced use cases:

### Block Collapse

```tsx
import {
  collapseBlocks,        // Transform AST to collapse specified blocks
  collectAllBlockIds,    // Get all block IDs from a block diagram AST
  collectBlockChildCounts, // Count children per block
  getBlockId,            // Get the canonical ID for a block
  slugifyBlockLabel,     // Slugify a block label for use as an ID
} from "@immediate-diagram/react";
```

### Syntax Highlighting

```tsx
import { tokenizeLine, tokensToHtml } from "@immediate-diagram/react";

const tokens = tokenizeLine("A --> B");
const html = tokensToHtml(tokens);
// <span class="imd-syn-identifier">A</span> <span class="imd-syn-arrow">--></span> ...
```

### SVG Processing

```tsx
import {
  extractSvgDimensions,  // Extract width/height from SVG viewBox
  injectViewportGroup,   // Inject a <g> wrapper for zoom/pan transforms
  sanitizeSvg,           // Sanitize SVG through DOMPurify
  DOMPURIFY_SVG_CONFIG,  // DOMPurify config optimized for SVG diagrams
} from "@immediate-diagram/react";
```

### Built-in Examples

```tsx
import { EXAMPLE_DIAGRAMS, EXAMPLE_TYPES } from "@immediate-diagram/react";

// 15 pre-built example diagrams covering all 7 diagram types
EXAMPLE_DIAGRAMS.forEach(({ id, type, title, description, source, tags }) => {
  console.log(`${type}: ${title}`);
});
```

---

## Theming

### Provider-Level Theme

Set the theme for all diagrams in the tree:

```tsx
// Preset
<DiagramProvider theme="dark">

// Partial custom overrides (merged with defaults)
<DiagramProvider theme={{ name: "custom", background: "#1a1a2e", nodeStroke: "#e94560" }}>
```

### Per-Diagram Theme via DSL

Individual diagrams can declare their theme in the DSL:

```
flowchart
theme dark
A --> B --> C
```

When `DiagramProvider` has no explicit `theme` prop, the DSL `theme` directive takes effect.

### System Theme Detection

```tsx
import { useThemePreference, usePageTheme } from "@immediate-diagram/react";

// Full preference management (with localStorage persistence)
const { theme, toggleTheme } = useThemePreference();

// Read-only page theme tracking (watches <html data-theme="...">)
const pageTheme = usePageTheme();
```

---

## Examples

### Basic Rendering

```tsx
import { ImmediateDiagram } from "@immediate-diagram/react";

function BasicExample() {
  return (
    <ImmediateDiagram
      dsl={`flowchart
  Start("Start") --> Check{"Ready?"}
  Check -->|Yes| Deploy["Deploy 🚀"]
  Check -->|No| Wait("Wait") --> Check`}
    />
  );
}
```

### Dark Theme

```tsx
<ImmediateDiagram
  dsl={`sequence
  Client ->> Server: POST /api/login
  Server ->> DB: SELECT user
  DB -->> Server: User record
  Server -->> Client: 200 JWT token`}
  theme="dark"
/>
```

### Interactive Viewer with Zoom/Pan

```tsx
<ImmediateDiagram
  dsl={`architecture
  group "Frontend" {
    React["React App"]
    Redux["State"]
  }
  group "Backend" {
    API["REST API"]
    DB[("Database")]
  }
  React --> API
  Redux --> React
  API --> DB`}
  interactive
  showZoomControls
  initialZoom="fit"
/>
```

### Timeline Playback

```tsx
<ImmediateDiagram
  dsl={`pie "Market Share"
  "Chrome": 65
  "Safari": 19
  "Firefox": 4
  "Edge": 4
  "Other": 8

  @state "2023" {
    "Chrome": 63
    "Safari": 20
    "Firefox": 5
  }

  @state "2024" {
    "Chrome": 65
    "Safari": 19
    "Firefox": 4
  }

  @timeline "growth" {
    default --> "2023" --> "2024"
  }`}
  showControls
  autoPlay
/>
```

### Custom Viewer with Hooks

Build a completely custom diagram viewer using the lower-level hooks:

```tsx
import { parse, narrowDiagram } from "@immediate-diagram/parser";
import { renderDiagram } from "@immediate-diagram/renderer";
import {
  DiagramProvider,
  useDiagramContext,
  useEffectiveTheme,
} from "@immediate-diagram/react";

function CustomViewer({ source }: { source: string }) {
  const { measureText, layoutEngine, fontsReady } = useDiagramContext();
  const doc = parse(source);
  const ast = narrowDiagram(doc);
  const theme = useEffectiveTheme(ast.metadata);

  if (!fontsReady) return <p>Loading fonts…</p>;

  const positioned = layoutEngine.layout(ast, { measureText });
  const svg = renderDiagram(positioned, { theme });

  return <div dangerouslySetInnerHTML={{ __html: svg }} />;
}

function App() {
  return (
    <DiagramProvider theme="dark">
      <CustomViewer source="flowchart\n  A --> B" />
    </DiagramProvider>
  );
}
```

### Multiple Diagrams on One Page

Share a single provider for efficient resource reuse:

```tsx
import { DiagramProvider, DiagramViewerFromSource } from "@immediate-diagram/react";

const diagrams = [
  "flowchart\n  A --> B --> C",
  "pie\n  \"React\": 60\n  \"Vue\": 25\n  \"Angular\": 15",
  "venn\n  {A} \"Frontend\"\n  {B} \"Backend\"\n  {A,B} \"Fullstack\"",
];

function Dashboard() {
  return (
    <DiagramProvider>
      {diagrams.map((dsl, i) => (
        <DiagramViewerFromSource key={i} source={dsl} lazy />
      ))}
    </DiagramProvider>
  );
}
```

Use `lazy` to defer layout computation for off-screen diagrams.

### Error Handling

```tsx
import { ImmediateDiagram } from "@immediate-diagram/react";

<ImmediateDiagram
  dsl={userInput}
  onError={(error, info) => {
    console.error("Render error:", error);
    reportToSentry(error, info);
  }}
  errorFallback={(error, reset) => (
    <div style={{ padding: 16, color: "red" }}>
      <p>Failed to render: {error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
  renderError={(error, source) => (
    <pre style={{ color: "orange" }}>
      Parse error at line {error.location?.start?.line}: {error.message}
    </pre>
  )}
/>
```

### Lazy Loading

For pages with many diagrams, enable lazy rendering to avoid computing layout for off-screen content:

```tsx
<DiagramProvider>
  {allDiagrams.map((ast, i) => (
    <DiagramViewer
      key={i}
      ast={ast}
      lazy
      lazyRootMargin="200px"  // Start rendering 200px before viewport entry
    />
  ))}
</DiagramProvider>
```

### Copy to Clipboard

```tsx
import { useRef } from "react";
import { DiagramProvider, DiagramViewer, CopyImageButton } from "@immediate-diagram/react";

function CopyableViewer({ ast }) {
  const containerRef = useRef<HTMLDivElement>(null);

  return (
    <DiagramProvider>
      <div ref={containerRef} style={{ position: "relative" }}>
        <DiagramViewer ast={ast} />
        <CopyImageButton
          containerRef={containerRef}
          position="top-right"
          pixelRatio={2}
          backgroundColor="#ffffff"
        />
      </div>
    </DiagramProvider>
  );
}
```

---

## CSS (Optional)

Import the optional stylesheet for enhanced visual styling:

```tsx
import "@immediate-diagram/react/styles.css";
```

All components work without this import (graceful degradation). The CSS provides:

- `.imd-syn-*` — Syntax highlighting colors for IMD DSL (light + dark themes)
- `.theme-toggle` — Theme toggle button styling with hover effects
- `.toast-*` — Toast notification animations and variant colors
- `.loading-spinner-*` — Animated loading indicator
- `@media (prefers-reduced-motion)` — Accessibility: disables animations when user prefers reduced motion

---

## Migration from @immediate-diagram/web

If you're currently importing diagram components from `@immediate-diagram/web`, migration is straightforward:

### 1. Install the react package

```bash
npm install @immediate-diagram/react
```

### 2. Update imports

```diff
- import { ImmediateDiagram } from "@immediate-diagram/web";
+ import { ImmediateDiagram } from "@immediate-diagram/react";

- import { DiagramViewer, DiagramProvider } from "@immediate-diagram/web";
+ import { DiagramViewer, DiagramProvider } from "@immediate-diagram/react";

- import "@immediate-diagram/web/styles.css";
+ import "@immediate-diagram/react/styles.css";
```

### 3. API compatibility

All component props, hook signatures, and context APIs are identical. The react package is a direct extraction of the diagram components from the web package — no breaking changes.

### 4. What stays in `@immediate-diagram/web`

The web package retains page-level concerns: routing, authentication, dashboard, editor UI. The react package contains all reusable diagram rendering components, hooks, and utilities.

---

## TypeScript Support

This package is written in TypeScript and ships `.ts` source files. All components, hooks, props interfaces, and utility types are fully typed and exported.

Key exported types:

```tsx
import type {
  // Component props
  ImmediateDiagramProps,
  ImmediateDiagramRef,
  DiagramViewerProps,
  DiagramViewerFromSourceProps,
  InteractiveDiagramViewerProps,
  PlaybackControllerProps,
  ZoomControlsProps,
  CopyImageButtonProps,
  DiagramTooltipProps,
  ThemeToggleProps,
  DiagramWrapperProps,
  DiagramErrorBoundaryProps,
  RecoveryErrorPanelProps,
  LoadingSpinnerProps,
  ConfirmDialogProps,
  ToastMessage,
  ToastContainerProps,
  TooltipData,

  // Context
  DiagramProviderProps,
  DiagramContextValue,
  DiagramSkeletonProps,
  SkeletonVariant,

  // Hook return types
  PlaybackControllerState,
  SpeedOption,
  RecoveryParseState,
  UseBlockCollapseStateReturn,
  UseViewportTransformReturn,
  ViewportState,
  UseLazyRenderReturn,
  UseSplitPaneReturn,
  ThemePreference,
  UseTimelinePlaybackReturn,
  CopyImageStatus,
  UseCopyDiagramAsImageReturn,

  // Utility types
  SyntaxToken,
  ExampleDiagram,
  ExampleTypeFilter,
  ZoomControlsPosition,
  CopyImageButtonPosition,
  Point,
} from "@immediate-diagram/react";
```

---

## Browser Support

- **Chrome / Edge**: 90+
- **Firefox**: 90+
- **Safari**: 15+
- **Mobile Safari**: 15+
- **Chrome Android**: 90+

Requires:
- `IntersectionObserver` (for `lazy` rendering — falls back to eager rendering)
- `ResizeObserver` (for fit-to-view calculations — falls back gracefully)
- Canvas API (for text measurement — falls back to heuristic estimation)
- Clipboard API (for copy-to-clipboard — falls back to file download)

---

## Troubleshooting

### "Cannot find module '@immediate-diagram/react'"

Ensure you've installed the package and its peer dependencies:

```bash
npm install @immediate-diagram/react react react-dom
```

### Diagram renders with wrong text sizing

Text measurement depends on the Canvas API and font loading. If custom fonts haven't loaded yet, the component shows a skeleton placeholder. If text sizing is consistently wrong, check that the `fontFamily` prop on `DiagramProvider` matches the fonts used in your app.

### Blank diagram (no SVG output)

1. Check the browser console for parse errors.
2. Verify the DSL syntax — use `DiagramViewerFromSource` which shows parse errors automatically.
3. Ensure `DiagramProvider` wraps your viewer components (not needed for `ImmediateDiagram`).

### Interactive zoom/pan not working

- Ensure `interactive={true}` is set on `ImmediateDiagram` or use `InteractiveDiagramViewer`.
- For keyboard shortcuts, the diagram container must have focus (`tabIndex={0}` is set automatically).
- On mobile, touch events require the diagram container to be large enough to interact with.

### DOMPurify errors in SSR / Node.js

DOMPurify requires a DOM environment. In SSR contexts, either:
1. Set `sanitize={false}` (the default) — the rendering pipeline already escapes user text.
2. Use `isomorphic-dompurify` in your SSR setup.

### Timeline playback controls not showing

The playback controller only appears when the diagram has `@state` or `@timeline` blocks. With 0–1 states, no controls are rendered. With 2 states, a compact toggle appears. With 3+ states, the full controller is shown.

### Theme mismatch between page and diagram

Use `DiagramProvider theme="dark"` to match your page theme, or let diagrams control their own theme via the DSL `theme dark` directive. The `DiagramWrapper` automatically adds a visual boundary when themes differ.

### Performance with many diagrams

For pages with 20+ diagrams, enable lazy rendering:

```tsx
<DiagramViewer ast={ast} lazy lazyRootMargin="200px" />
```

This defers layout computation until each diagram scrolls into view.

---

## API Reference

All public APIs are documented with JSDoc in the source files. The exports are organized into four categories:

| Category | Exports |
|----------|---------|
| **Components** | `ImmediateDiagram`, `DiagramViewer`, `DiagramViewerFromSource`, `InteractiveDiagramViewer`, `PlaybackController`, `ZoomControls`, `CopyImageButton`, `DiagramTooltip`, `ThemeToggle`, `ToastContainer`, `ConfirmDialog`, `DiagramWrapper`, `DiagramErrorBoundary`, `RecoveryErrorPanel`, `LoadingSpinner`, `Button` |
| **Context** | `DiagramProvider`, `DiagramSkeleton`, `useDiagramContext`, `useEffectiveTheme`, `useDiagramTheme`, `useFontsReady`, `useLayoutEngine`, `useMeasureText`, `checkFontAvailable`, `extractPrimaryFont` |
| **Hooks** | `usePlaybackController`, `useRecoveryParse`, `useTimelinePlayback`, `useBlockCollapseState`, `useViewportTransform`, `usePinchZoom`, `useViewportKeyboard`, `useDebouncedValue`, `useThemePreference`, `usePageTheme`, `useReducedMotion`, `useLazyRender`, `useCopyDiagramAsImage`, `useSplitPane`, `useToast` |
| **Utilities** | `collapseBlocks`, `collectAllBlockIds`, `collectBlockChildCounts`, `getBlockId`, `slugifyBlockLabel`, `tokenizeLine`, `tokensToHtml`, `extractSvgDimensions`, `injectViewportGroup`, `sanitizeSvg`, `DOMPURIFY_SVG_CONFIG`, `clampScale`, `computeCursorZoom`, `computeFitToView`, `touchDistance`, `touchMidpoint`, `computePinchScale`, `isClipboardWriteSupported`, `downloadBlob`, `rasterizeSvgToBlob`, `postProcessCollapseIndicators`, `EXAMPLE_DIAGRAMS`, `EXAMPLE_TYPES`, `SPEED_OPTIONS`, `PAN_STEP`, `PAN_FAST_STEP`, `COLLAPSED_BLOCK_WIDTH`, `COLLAPSED_BLOCK_HEIGHT` |

For detailed type information, see the TypeScript source at `src/index.ts` and the individual module files.

---

## License

MIT