> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Lexical Editor

> Setup Multiplayer Editing for Lexical Editor.

The `@veltdev/lexical-crdt-react` and `@veltdev/lexical-crdt` libraries enable real-time collaborative editing on Lexical Editors. The collaboration editing engine is built on top of [Yjs](https://docs.yjs.dev/), the official `@lexical/yjs` binding, and Velt SDK.

## Prerequisites

* Node.js (v14 or higher)
* A Velt account with an API key ([sign up](https://console.velt.dev/))
* React (v18 or higher) when using the React wrapper
* Optional: TypeScript for type safety

## Setup

### Step 1: Install Dependencies

Install the required packages:

<Tabs>
  <Tab title="React / Next.js">
    ```bash theme={null}
    npm install @veltdev/lexical-crdt-react @veltdev/lexical-crdt @veltdev/react lexical @lexical/react @lexical/rich-text @lexical/yjs yjs y-protocols
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```bash theme={null}
    npm install @veltdev/lexical-crdt @veltdev/client lexical @lexical/rich-text @lexical/yjs yjs
    ```

    Install any additional `@lexical/*` node packages required by your schema. Keep one copy of `lexical`, every `@lexical/*` package, and `yjs` in the final application bundle.
  </Tab>
</Tabs>

### Step 2: Setup Velt

Initialize Velt, authenticate a user, and set a stable document context before collaboration starts. Follow the [Velt Setup Docs](/docs/get-started/quickstart) for the production token endpoint used by `authProvider`.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    import { useEffect, useState } from 'react';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';

    const DOCUMENT_ID = 'my-lexical-editor';
    const USER = {
      userId: 'user-1',
      name: 'John Doe',
      email: 'john@example.com',
      organizationId: 'org-1',
    };

    const authProvider = {
      user: USER,
      retryConfig: { retryCount: 3, retryDelay: 1000 },
      generateToken: async () => {
        const response = await fetch('/api/velt/token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(USER),
        });
        const { token } = await response.json();
        return token;
      },
    };

    function CollaborationScope() {
      const user = useCurrentUser();
      const { setDocuments } = useSetDocuments();
      const [documentReady, setDocumentReady] = useState(false);

      useEffect(() => {
        if (!user) {
          setDocumentReady(false);
          return;
        }
        setDocuments([{
          id: DOCUMENT_ID,
          metadata: { documentName: 'Shared Lexical Document' },
        }]);
        setDocumentReady(true);
      }, [user, setDocuments]);

      return user && documentReady
        ? <LexicalEditor documentId={DOCUMENT_ID} />
        : <p>Preparing collaboration...</p>;
    }

    <VeltProvider apiKey="YOUR_API_KEY" authProvider={authProvider}>
      <CollaborationScope />
    </VeltProvider>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Create one Velt client, set the shared document, authenticate the user, and wait for Velt before creating Lexical:

    ```ts theme={null}
    import { initVelt } from '@veltdev/client';

    const DOCUMENT_ID = 'my-lexical-editor';
    const client = await initVelt('YOUR_API_KEY');

    client.setDocument(DOCUMENT_ID, {
      documentName: 'Shared Lexical Document',
    });

    await client.identify({
      userId: 'user-1',
      name: 'John Doe',
      email: 'john@example.com',
      organizationId: 'org-1',
    });

    const initSubscription = client.getVeltInitState().subscribe((isReady) => {
      if (isReady) void initializeLexical(client);
    });
    ```
  </Tab>
</Tabs>

### Step 3: Initialize Collaborative Editor

* Set the Lexical composer's `editorState` to `null` because collaboration owns the document state.
* Add the collaboration hook or plugin inside `<LexicalComposer>` so it can read the editor from `LexicalComposerContext`.
* Pass an `editorId` to uniquely identify each editor instance you have in your app.

<Tabs>
  <Tab title="React Component">
    Use `LexicalCollaborationPlugin` when you only need lifecycle wiring and do not need to render reactive collaboration state:

    ```tsx theme={null}
    import { LexicalComposer } from '@lexical/react/LexicalComposer';
    import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
    import { ContentEditable } from '@lexical/react/LexicalContentEditable';
    import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
    import { LexicalCollaborationPlugin } from '@veltdev/lexical-crdt-react';

    const initialConfig = {
      namespace: 'my-collab-editor',
      editorState: null,
      onError: console.error,
    };

    function LexicalEditor() {
      return (
        <LexicalComposer initialConfig={initialConfig}>
          <LexicalCollaborationPlugin editorId="my-lexical-editor" />
          <RichTextPlugin
            contentEditable={<ContentEditable className="lexical-editor" />}
            placeholder={<div>Start typing...</div>}
            ErrorBoundary={LexicalErrorBoundary}
          />
        </LexicalComposer>
      );
    }
    ```
  </Tab>

  <Tab title="React Hook">
    Use `useLexicalComposerCollaboration()` inside the composer when you need reactive collaboration state.

    ```tsx theme={null}
    import { LexicalComposer } from '@lexical/react/LexicalComposer';
    import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
    import { ContentEditable } from '@lexical/react/LexicalContentEditable';
    import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
    import { HeadingNode, QuoteNode } from '@lexical/rich-text';
    import { useLexicalComposerCollaboration } from '@veltdev/lexical-crdt-react';

    const theme = {
      collaboration: {
        cursor: 'lexical-collaboration-cursor',
        cursorName: 'lexical-collaboration-cursor-name',
        selection: 'lexical-collaboration-selection',
        selectionBg: 'lexical-collaboration-selection-bg',
      },
    };

    const initialConfig = {
      namespace: 'my-collab-editor',
      nodes: [HeadingNode, QuoteNode],
      theme,
      editorState: null,
      onError: console.error,
    };

    function CollaborationBridge({ documentId }: { documentId: string }) {
      const { isLoading, isSynced, status, error } = useLexicalComposerCollaboration({
        editorId: documentId,
        cursorData: {
          name: 'Michael',
          awarenessData: { userId: 'michael' },
        },
      });

      if (error) return <div>Error: {error.message}</div>;
      return <div>Status: {isLoading ? 'loading' : status} {isSynced ? '(synced)' : ''}</div>;
    }

    function LexicalEditor({ documentId }: { documentId: string }) {
      return (
        <LexicalComposer initialConfig={initialConfig}>
          <CollaborationBridge documentId={documentId} />
          <RichTextPlugin
            contentEditable={<ContentEditable className="lexical-editor" />}
            placeholder={<div>Start typing...</div>}
            ErrorBoundary={LexicalErrorBoundary}
          />
        </LexicalComposer>
      );
    }
    ```

    <Warning>Set `editorState: null` in the Lexical composer. Providing a separate composer state can conflict with the shared CRDT document.</Warning>
  </Tab>

  <Tab title="Other Frameworks">
    Create and configure Lexical first, attach its root element, and only then pass the editor to `createCollaboration()`:

    ```ts theme={null}
    import { createEditor } from 'lexical';
    import {
      HeadingNode,
      QuoteNode,
      registerRichText,
    } from '@lexical/rich-text';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/lexical-crdt';

    let editor: ReturnType<typeof createEditor> | null = null;
    let manager: CollaborationManager | null = null;
    let unregisterRichText: (() => void) | null = null;
    let offStatus: (() => void) | null = null;
    let offSynced: (() => void) | null = null;

    async function initializeLexical(client) {
      if (manager) return;

      editor = createEditor({
        namespace: 'my-collab-editor',
        nodes: [HeadingNode, QuoteNode],
        onError: console.error,
      });

      editor.setRootElement(document.querySelector('#editor'));
      unregisterRichText = registerRichText(editor);

      manager = await createCollaboration({
        editorId: DOCUMENT_ID,
        editor,
        veltClient: client,
        cursorData: { name: 'John Doe', color: '#7c3aed' },
        onError: (error) => console.error('Collaboration error:', error),
      });

      offStatus = manager.onStatusChange((status) => console.log('Status:', status));
      offSynced = manager.onSynced((synced) => console.log('Synced:', synced));
    }

    function destroyLexical() {
      offStatus?.();
      offSynced?.();
      manager?.destroy();
      unregisterRichText?.();
      editor?.setRootElement(null);
      manager = null;
      editor = null;
    }
    ```

    Do not register Lexical's history plugin. The collaboration manager exposes the binding's `Y.UndoManager` through `manager.getUndoManager()` so local undo/redo remains collaboration-aware.
  </Tab>
</Tabs>

### Step 4: Use an Explicit Editor Instance in React (Optional)

If you create and hold a `LexicalEditor` instance outside `LexicalComposer`, use `useCollaboration()` and pass the instance explicitly:

```tsx theme={null}
import { useCollaboration } from '@veltdev/lexical-crdt-react';

const { primitives, isLoading, isSynced, status, error, manager } = useCollaboration({
  editorId: 'my-lexical-editor',
  editor: myLexicalEditor,
});
```

### Step 5: Status Monitoring (Optional)

<Tabs>
  <Tab title="React / Next.js">
    Both hooks return reactive `status`, `isSynced`, and `error` state:

    ```tsx theme={null}
    const { isLoading, isSynced, status, error, manager } =
      useLexicalComposerCollaboration({
        editorId: 'my-lexical-editor',
      });

    if (error) return <div>Error: {error.message}</div>;
    if (isLoading) return <div>Connecting...</div>;

    // In your JSX
    <div>Status: {status} | Synced: {isSynced ? 'Yes' : 'No'}</div>
    ```

    For event-style subscriptions, use the manager:

    ```tsx theme={null}
    useEffect(() => {
      if (!manager) return;

      const offStatus = manager.onStatusChange((nextStatus) => {
        console.log('Status:', nextStatus);
      });
      const offSynced = manager.onSynced((synced) => {
        console.log('Synced:', synced);
      });

      return () => {
        offStatus();
        offSynced();
      };
    }, [manager]);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Read the current values or subscribe to changes on the manager. Save and call both unsubscribe functions during cleanup:

    ```ts theme={null}
    console.log(manager.initialized, manager.status, manager.synced);

    const offStatus = manager.onStatusChange((status) => {
      console.log('Status:', status);
    });

    const offSynced = manager.onSynced((synced) => {
      console.log('Synced:', synced);
    });

    // During teardown
    offStatus();
    offSynced();
    ```
  </Tab>
</Tabs>

### Step 6: Version Management (Optional)

Save and restore named snapshots through the manager. Restored versions are applied to every connected user in real time.

```ts theme={null}
if (!manager) return;

const versionId = await manager.saveVersion('Before major edit');
const versions = await manager.getVersions();
await manager.restoreVersion(versionId);
```

When your application already has the complete `Version` object, use the lower-level state application API instead of `restoreVersion()`:

```ts theme={null}
const selectedVersion = versions.find((version) => version.versionId === versionId);
if (selectedVersion) {
  await manager.setStateFromVersion(selectedVersion);
}
```

### Step 7: Force Reset Initial Content (Optional)

By default, `initialContent` is applied exactly once when the shared document is brand new. Use a stringified serialized Lexical editor state. React wrappers may also accept the wrapper's documented plain-text convenience form.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    useLexicalComposerCollaboration({
      editorId: 'my-lexical-editor',
      initialContent,
      forceResetInitialContent: true,
    });
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      editor,
      veltClient: client,
      initialContent: JSON.stringify({
        root: {
          children: [{
            children: [{
              detail: 0,
              format: 0,
              mode: 'normal',
              style: '',
              text: 'Start writing…',
              type: 'text',
              version: 1,
            }],
            direction: null,
            format: '',
            indent: 0,
            type: 'paragraph',
            version: 1,
          }],
          direction: null,
          format: '',
          indent: 0,
          type: 'root',
          version: 1,
        },
      }),
      forceResetInitialContent: false,
    });
    ```
  </Tab>
</Tabs>

<Warning>Use `forceResetInitialContent` only for deliberate reset workflows. It clears the current shared document and replaces it with `initialContent` for every connected user.</Warning>

### Step 8: Style Collaboration Cursors

Remote carets, labels, and text selections render through Lexical's collaboration cursor layer. Pass user identity through `cursorData`, then style the Lexical collaboration theme classes:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    useLexicalComposerCollaboration({
      editorId: 'my-lexical-editor',
      cursorData: {
        name: user.name,
        color: '#7c3aed',
        awarenessData: { userId: user.userId },
      },
    });
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      editor,
      veltClient: client,
      cursorData: {
        name: 'John Doe',
        color: '#7c3aed',
        awarenessData: { role: 'editor' },
      },
    });
    ```
  </Tab>
</Tabs>

```css theme={null}
[data-lexical-cursors='true'] {
  pointer-events: none;
}

.lexical-collaboration-selection {
  mix-blend-mode: multiply;
  z-index: 4;
}

.lexical-collaboration-selection-bg {
  background-color: rgba(250, 204, 21, 0.4);
  border-radius: 2px;
}

.lexical-collaboration-cursor {
  background-color: var(--lexical-cursor-color, #7c3aed);
  width: 2px;
  z-index: 10;
}

.lexical-collaboration-cursor-name {
  position: absolute;
  top: -1.4rem;
  left: -1px;
  padding: 0.15rem 0.35rem;
  color: #fff;
  background-color: var(--lexical-cursor-color, #7c3aed);
  border-radius: 3px;
  font-size: 0.7rem;
  white-space: nowrap;
}
```

### Step 9: CRDT Event Subscription (Optional)

Listen for real-time CRDT data events using `getCrdtElement().on()` from the Velt client.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    import { useEffect } from 'react';
    import { useVeltClient } from '@veltdev/react';

    const { client } = useVeltClient();

    useEffect(() => {
      if (!client) return;

      const subscription = client.getCrdtElement().on('updateData').subscribe((event) => {
        if (event) console.log('CRDT update:', event);
      });

      return () => subscription?.unsubscribe?.();
    }, [client]);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const crdtSubscription = client
      .getCrdtElement()
      .on('updateData')
      .subscribe((event) => {
        if (event) console.log('CRDT update:', event);
      });

    // During teardown
    crdtSubscription.unsubscribe();
    ```
  </Tab>
</Tabs>

### Step 10: Custom Encryption (Optional)

Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider. For CRDT methods, input data is of type `Uint8Array | number[]`.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const encryptionProvider = {
      encrypt: async ({ data }) => data.map((n) => n.toString()).join('__'),
      decrypt: async ({ data }) => data.split('__').map((s) => parseInt(s, 10)),
    };

    <VeltProvider
      apiKey="YOUR_API_KEY"
      authProvider={authProvider}
      encryptionProvider={encryptionProvider}
    >
      ...
    </VeltProvider>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Call `setEncryptionProvider()` before creating the collaboration manager:

    ```ts theme={null}
    client.setEncryptionProvider({
      encrypt: async ({ data }) => data.map((n) => n.toString()).join('__'),
      decrypt: async ({ data }) => data.split('__').map((s) => parseInt(s, 10)),
    });
    ```
  </Tab>
</Tabs>

See also: [setEncryptionProvider()](/docs/api-reference/sdk/api/api-methods#setencryptionprovider-encryptionprovider)
· [VeltEncryptionProvider](/docs/api-reference/sdk/models/data-models#veltencryptionprovider)
· [EncryptConfig](/docs/api-reference/sdk/models/data-models#encryptconfig)
· [DecryptConfig](/docs/api-reference/sdk/models/data-models#decryptconfig)

### Step 11: Error Handling (Optional)

Pass an `onError` callback to handle initialization or runtime errors. The reactive `error` state is also available for rendering.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const collab = useLexicalComposerCollaboration({
      editorId: 'my-lexical-editor',
      onError: (error) => console.error('Collaboration error:', error),
    });

    if (collab.error) {
      return <div>Error: {collab.error.message}</div>;
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      editor,
      veltClient: client,
      onError: (error) => console.error('Lexical collaboration error:', error),
    });
    ```

    The package reports unexpected failures through `onError`; manager getters and version methods return null-safe or empty fallback values instead of throwing.
  </Tab>
</Tabs>

### Step 12: Access Yjs Internals (Advanced)

The hooks return lower-level Lexical, Yjs, and Velt handles through `primitives`:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const { primitives, manager } = useLexicalComposerCollaboration({
      editorId: 'my-lexical-editor',
    });

    const editor = primitives?.editor;
    const binding = primitives?.binding;
    const doc = primitives?.doc;
    const xmlText = primitives?.xmlText;
    const provider = primitives?.provider;
    const lexicalProvider = primitives?.lexicalProvider;
    const awareness = primitives?.awareness;
    const undoManager = primitives?.undoManager;
    const store = primitives?.store;

    // The same handles are available from the manager.
    manager?.getEditor();
    manager?.getBinding();
    manager?.getDoc();
    manager?.getXmlText();
    manager?.getProvider();
    manager?.getLexicalProvider();
    manager?.getAwareness();
    manager?.getUndoManager();
    manager?.getStore();
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const editor = manager.getEditor();
    const binding = manager.getBinding();
    const doc = manager.getDoc();
    const xmlText = manager.getXmlText();
    const provider = manager.getProvider();
    const lexicalProvider = manager.getLexicalProvider();
    const awareness = manager.getAwareness();
    const undoManager = manager.getUndoManager();
    const store = manager.getStore();
    ```
  </Tab>
</Tabs>

Let the manager own binding setup, provider attachment, awareness, and Yjs history. Do not wire `@lexical/yjs` manually alongside it.

### Step 13: Cleanup

<Tabs>
  <Tab title="React / Next.js">
    The hooks destroy the manager automatically when the component unmounts or when the `editorId`, editor, or Velt client changes. Pass `enabled: false` to tear down collaboration early while keeping the component mounted.

    ```tsx theme={null}
    const collab = useLexicalComposerCollaboration({
      editorId: 'my-lexical-editor',
      enabled: collaborationEnabled,
    });
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Unsubscribe callbacks and destroy collaboration before discarding the Lexical editor:

    ```ts theme={null}
    crdtSubscription.unsubscribe();
    offStatus();
    offSynced();
    initSubscription.unsubscribe();

    manager.destroy();
    unregisterRichText();
    editor.setRootElement(null);
    ```

    `manager.destroy()` removes the cursor overlay, provider/store resources, binding, undo manager, and listeners. It is safe to call more than once.
  </Tab>
</Tabs>

## Notes

* **Unique editorId**: Use a unique `editorId` per editor instance.
* **Composer state**: Set `editorState: null` because collaboration owns the shared Lexical state.
* **Composer hook**: Use `useLexicalComposerCollaboration()` inside `<LexicalComposer>`.
* **Explicit editor**: Use `useCollaboration()` only when you already hold a `LexicalEditor` instance.
* **Plugin form**: Use `LexicalCollaborationPlugin` when you do not need reactive status, error, or manager values.
* **Cursor identity**: Pass `cursorData` so remote carets and selections can display user information.
* **Lifecycle**: The hooks wait for the Lexical root element, Velt initialization, and an authenticated user before creating the manager.
* **Other Frameworks lifecycle**: Create and attach Lexical first, then create one manager; destroy the manager before discarding the editor.
* **Collaborative history**: Do not register Lexical's normal history plugin. Use the binding's Yjs-backed undo manager from `manager.getUndoManager()`.
* **Debounced writes**: The Store default is `debounceMs: 0`, so backend writes are immediate when the option is omitted. Lexical's separate snapshot-flush scheduler falls back to 250 ms after applying seeded or prefetched snapshot state.
* **Dependency deduplication**: Keep one copy of `lexical`, each `@lexical/*` package, and `yjs` in the application bundle.
* **Production authentication**: Do not expose privileged server tokens in browser code; use Velt's recommended server-backed authentication flow.

## Testing and Debugging

**To test collaboration:**

1. Log in as two unique users in two separate browser profiles and open the same Velt document and Lexical `editorId` in both.
2. Edit text, formatting, headings, or lists in one window and verify the changes and remote cursor appear in the other.
3. Make concurrent edits and verify they merge at the character level.
4. Reload both windows and verify the shared document persists.

**Common issues:**

* Editor not loading: Confirm the Velt client is initialized, the user is authenticated, and the collaboration bridge renders inside `<LexicalComposer>`.
* Initial content conflicts: Set the composer's `editorState` to `null`.
* Cursors not appearing: Pass `cursorData` and include the Lexical collaboration theme classes in your editor theme and CSS.
* Changes not syncing: Confirm both users have the same Velt document context and `editorId`.
* Manual reconnect: Use `manager.getProvider()?.connect()` to reconnect or `manager.getProvider()?.disconnect()` to test an offline transition.

<Tip>
  Enable browser console warnings to see detailed debugging information. You can also use the [VeltCrdtStoreMap debugging interface](/docs/realtime-collaboration/crdt/setup/core#veltcrdtstoremap) to inspect and monitor your CRDT stores in real time.
</Tip>

## Complete Example

<Tabs>
  <Tab title="React / Next.js">
    A complete collaborative Lexical editor with user login, status display, cursor identity, and version management.

    **App.tsx**

    ```tsx Complete App.tsx expandable lines theme={null}
    import { useEffect, useMemo, useState } from 'react';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';
    import { CollaborativeEditor, DOCUMENT_ID } from './Editor';

    const USER = {
      userId: 'michael',
      name: 'Michael Scott',
      email: 'michael.scott@dundermifflin.com',
      organizationId: 'my-org-id',
      color: '#7c3aed',
    };

    function AppContent() {
      const veltUser = useCurrentUser();
      const { setDocuments } = useSetDocuments();
      const [documentReady, setDocumentReady] = useState(false);

      useEffect(() => {
        if (!veltUser) {
          setDocumentReady(false);
          return;
        }
        setDocuments([{
          id: DOCUMENT_ID,
          metadata: { documentName: 'Lexical CRDT React Demo' },
        }]);
        setDocumentReady(true);
      }, [veltUser, setDocuments]);

      return (
        <div>
          {veltUser && documentReady ? (
            <CollaborativeEditor key={veltUser.userId} />
          ) : (
            <p>Preparing collaboration...</p>
          )}
        </div>
      );
    }

    export default function App() {
      const authProvider = useMemo(() => ({
        user: USER,
        retryConfig: { retryCount: 3, retryDelay: 1000 },
        generateToken: async () => {
          const response = await fetch('/api/velt/token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(USER),
          });
          const { token } = await response.json();
          return token;
        },
      }), []);

      return (
        <VeltProvider apiKey="YOUR_API_KEY" authProvider={authProvider}>
          <AppContent />
        </VeltProvider>
      );
    }
    ```

    **Editor.tsx**

    ```tsx Complete Editor.tsx expandable lines theme={null}
    import { useState } from 'react';
    import { useCurrentUser } from '@veltdev/react';
    import { LexicalComposer } from '@lexical/react/LexicalComposer';
    import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
    import { ContentEditable } from '@lexical/react/LexicalContentEditable';
    import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
    import { HeadingNode, QuoteNode } from '@lexical/rich-text';
    import { useLexicalComposerCollaboration } from '@veltdev/lexical-crdt-react';

    export const DOCUMENT_ID = 'lexical-crdt-react-demo-doc-1';

    const theme = {
      collaboration: {
        cursor: 'lexical-collaboration-cursor',
        cursorName: 'lexical-collaboration-cursor-name',
        selection: 'lexical-collaboration-selection',
        selectionBg: 'lexical-collaboration-selection-bg',
      },
    };

    function CollaborationBridge() {
      const veltUser = useCurrentUser();
      const [versionName, setVersionName] = useState('');

      const {
        isLoading,
        isSynced,
        status,
        error,
        manager,
      } = useLexicalComposerCollaboration({
        editorId: DOCUMENT_ID,
        cursorData: veltUser
          ? {
              name: veltUser.name,
              color: '#7c3aed',
              awarenessData: { userId: veltUser.userId },
            }
          : undefined,
        onError: (err) => console.error('Collaboration error:', err),
      });

      const saveVersion = async (event: React.FormEvent) => {
        event.preventDefault();
        if (!manager || !versionName.trim()) return;
        await manager.saveVersion(versionName.trim());
        setVersionName('');
      };

      if (error) return <div>Error: {error.message}</div>;

      return (
        <div>
          <div>Status: {isLoading ? 'loading' : status} {isSynced ? '(synced)' : ''}</div>
          <form onSubmit={saveVersion}>
            <input
              value={versionName}
              onChange={(event) => setVersionName(event.target.value)}
              placeholder="Version name..."
            />
            <button type="submit" disabled={!manager}>Save Version</button>
          </form>
        </div>
      );
    }

    export function CollaborativeEditor() {
      return (
        <LexicalComposer
          initialConfig={{
            namespace: 'lexical-collab-demo',
            nodes: [HeadingNode, QuoteNode],
            theme,
            editorState: null,
            onError: console.error,
          }}
        >
          <CollaborationBridge />
          <RichTextPlugin
            contentEditable={<ContentEditable className="lexical-editor" />}
            placeholder={<div>Start typing...</div>}
            ErrorBoundary={LexicalErrorBoundary}
          />
        </LexicalComposer>
      );
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    This vanilla TypeScript example creates Lexical before the manager, uses collaborative undo/redo, monitors status, subscribes to CRDT events, and tears down every listener explicitly.

    ```html theme={null}
    <div id="status">Connecting...</div>
    <div id="editor" class="lexical-editor" contenteditable="true"></div>
    <button id="undo">Undo</button>
    <button id="redo">Redo</button>
    <button id="save-version">Save version</button>
    ```

    ```ts Complete main.ts expandable lines theme={null}
    import { createEditor, type LexicalEditor } from 'lexical';
    import {
      HeadingNode,
      QuoteNode,
      registerRichText,
    } from '@lexical/rich-text';
    import { initVelt } from '@veltdev/client';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/lexical-crdt';

    const API_KEY = 'YOUR_API_KEY';
    const DOCUMENT_ID = 'lexical-crdt-demo-doc-1';

    let editor: LexicalEditor | null = null;
    let manager: CollaborationManager | null = null;
    let unregisterRichText: (() => void) | null = null;
    let offStatus: (() => void) | null = null;
    let offSynced: (() => void) | null = null;
    let initSubscription: { unsubscribe(): void } | null = null;
    let crdtSubscription: { unsubscribe(): void } | null = null;

    const undo = () => manager?.getUndoManager()?.undo();
    const redo = () => manager?.getUndoManager()?.redo();
    const saveVersion = () => {
      void manager?.saveVersion('Manual checkpoint');
    };

    const initialContent = JSON.stringify({
      root: {
        children: [{
          children: [{
            detail: 0,
            format: 0,
            mode: 'normal',
            style: '',
            text: 'Start writing…',
            type: 'text',
            version: 1,
          }],
          direction: null,
          format: '',
          indent: 0,
          type: 'paragraph',
          version: 1,
        }],
        direction: null,
        format: '',
        indent: 0,
        type: 'root',
        version: 1,
      },
    });

    async function main() {
      const client = await initVelt(API_KEY);

      client.setDocument(DOCUMENT_ID, {
        documentName: 'Shared Lexical Document',
      });

      await client.identify({
        userId: 'ada',
        name: 'Ada Lovelace',
        email: 'ada@example.com',
        organizationId: 'my-org-id',
      });

      initSubscription = client.getVeltInitState().subscribe(async (isReady) => {
        if (!isReady || manager) return;

        editor = createEditor({
          namespace: 'lexical-collab-demo',
          nodes: [HeadingNode, QuoteNode],
          onError: console.error,
        });
        editor.setRootElement(document.querySelector('#editor'));
        unregisterRichText = registerRichText(editor);

        manager = await createCollaboration({
          editorId: DOCUMENT_ID,
          editor,
          veltClient: client,
          initialContent,
          cursorData: {
            name: 'Ada Lovelace',
            color: '#7c3aed',
            awarenessData: { userId: 'ada' },
          },
          onError: (error) => console.error('Collaboration error:', error),
        });

        const renderStatus = () => {
          const element = document.getElementById('status');
          if (element && manager) {
            element.textContent = `${manager.status} / synced: ${manager.synced}`;
          }
        };

        renderStatus();
        offStatus = manager.onStatusChange(renderStatus);
        offSynced = manager.onSynced(renderStatus);

        crdtSubscription = client
          .getCrdtElement()
          .on('updateData')
          .subscribe((event) => console.log('CRDT update:', event));

        document.getElementById('undo')?.addEventListener('click', undo);
        document.getElementById('redo')?.addEventListener('click', redo);
        document.getElementById('save-version')?.addEventListener('click', saveVersion);
      });
    }

    export function destroy() {
      document.getElementById('undo')?.removeEventListener('click', undo);
      document.getElementById('redo')?.removeEventListener('click', redo);
      document.getElementById('save-version')?.removeEventListener('click', saveVersion);
      crdtSubscription?.unsubscribe();
      crdtSubscription = null;
      offStatus?.();
      offStatus = null;
      offSynced?.();
      offSynced = null;
      initSubscription?.unsubscribe();
      initSubscription = null;

      manager?.destroy();
      manager = null;
      unregisterRichText?.();
      unregisterRichText = null;
      editor?.setRootElement(null);
      editor = null;
    }

    window.addEventListener('pagehide', destroy, { once: true });
    void main();
    ```

    Do not add Lexical's history plugin to this example. The remote cursor overlay is created and removed automatically by the collaboration manager.
  </Tab>
</Tabs>

## How It Works

1. **Velt initialization and authentication** happen through `VeltProvider` in React or `initVelt()` and `identify()` in other frameworks. Both paths establish the same stable document context.
2. **React wrappers** obtain the editor from `LexicalComposerContext` or accept an explicit editor. Other frameworks create the Lexical editor, register its nodes, and attach its root before creating collaboration.
3. **The collaboration manager** creates a Velt CRDT store backed by a Yjs document and a shared `Y.XmlText`.
4. **The official `@lexical/yjs` binding** connects Lexical updates to the shared `Y.XmlText`, so text, formatting, headings, and concurrent edits merge at the character level.
5. **Yjs awareness** drives the manager-owned remote cursor overlay, while the binding's `Y.UndoManager` provides collaboration-aware undo and redo.
6. **Version management** saves the complete Yjs state as a named snapshot that can be restored for every connected user.
7. **React cleanup** is automatic when the wrapper unmounts or collaboration is disabled. Other frameworks unsubscribe callbacks and destroy the manager before discarding Lexical.

## APIs

### React: useLexicalComposerCollaboration()

The primary hook for applications using `<LexicalComposer>`. It reads the editor from context, waits for the root element, and manages the collaboration lifecycle.

* Signature: `useLexicalComposerCollaboration(config: UseLexicalComposerCollaborationConfig): UseCollaborationReturn`
* Alias: `useLexicalCollaboration`
* Params:
  * `editorId`: Unique identifier for the collaborative editor.
  * `veltClient`: Optional explicit Velt client. Falls back to `VeltProvider` context.
  * `enabled`: Delay initialization or destroy active collaboration. Default: `true`.
  * `initialContent`: Serialized Lexical editor state or plain text for a brand-new document.
  * `forceResetInitialContent`: Replace existing shared content during initialization. Default: `false`.
  * `cursorData`: Cursor name, color, and custom awareness data.
  * `debounceMs`: Throttle interval in milliseconds for backend writes. Store default: `0` ms (immediate). The internal snapshot-flush scheduler separately falls back to 250 ms when this option is omitted.
  * `onError`: Callback for non-fatal collaboration errors.
* Returns:
  * `primitives`: Lexical, Yjs, and Velt integration handles. `null` while loading.
  * `isLoading`: `true` until the manager is initialized.
  * `isSynced`: `true` after initial backend sync completes.
  * `status`: `'connecting'`, `'connected'`, or `'disconnected'`.
  * `error`: Most recent error, or `null`.
  * `manager`: Underlying `CollaborationManager`, or `null` before initialization.

### React: useCollaboration()

Explicit-editor form of the React hook. It has the same lifecycle and return values, but requires an `editor: LexicalEditor` config value.

```tsx theme={null}
const collab = useCollaboration({
  editorId: 'my-lexical-editor',
  editor: myLexicalEditor,
});
```

### React: LexicalCollaborationPlugin

Null-rendering component form of `useLexicalComposerCollaboration()`. Use it when you only need collaboration lifecycle wiring.

```tsx theme={null}
<LexicalCollaborationPlugin editorId="my-lexical-editor" />
```

### Other Frameworks: createCollaboration()

The framework-agnostic `createCollaboration()` factory from `@veltdev/lexical-crdt` creates and initializes the manager for an existing `LexicalEditor`.

* Signature: `createCollaboration(config: CollaborationConfig): Promise<CollaborationManager>`
* Required config: `editorId`, `editor`, and an initialized, authenticated `veltClient`.
* Optional config: `initialContent`, `cursorData`, `debounceMs` (Store default: 0 ms), `forceResetInitialContent`, and `onError`. The internal snapshot-flush scheduler separately falls back to 250 ms when `debounceMs` is omitted.
* Cleanup: Call `manager.destroy()` before disposing the editor.

### LexicalCollaborationPrimitives

| Field             | Type                    | Description                                          |
| ----------------- | ----------------------- | ---------------------------------------------------- |
| `editor`          | `LexicalEditor \| null` | Bound Lexical editor instance.                       |
| `binding`         | `Binding \| null`       | Binding created by `@lexical/yjs`.                   |
| `lexicalProvider` | `Provider \| null`      | Lexical-compatible adapter around Velt's provider.   |
| `doc`             | `Y.Doc \| null`         | Shared Yjs document.                                 |
| `xmlText`         | `Y.XmlText \| null`     | Shared content type used by `@lexical/yjs`.          |
| `provider`        | `SyncProvider \| null`  | Velt real-time sync provider.                        |
| `awareness`       | `Awareness \| null`     | Yjs awareness instance for remote users and cursors. |
| `undoManager`     | `Y.UndoManager \| null` | Collaborative undo manager.                          |
| `store`           | `Store<string> \| null` | Underlying Velt CRDT store.                          |

### LexicalProviderAdapter

`LexicalProviderAdapter` is exported from `@veltdev/lexical-crdt`. It adapts Velt's `SyncProvider` events to the provider interface expected by `@lexical/yjs`; the collaboration manager creates it automatically, so most applications should access the existing adapter through `manager.getLexicalProvider()` instead of constructing another one.

### Version

| Field         | Type     | Description                                        |
| ------------- | -------- | -------------------------------------------------- |
| `versionId`   | `string` | Unique version identifier.                         |
| `versionName` | `string` | Human-readable version name.                       |
| `timestamp`   | `number` | Time the version was saved, in epoch milliseconds. |

### CollaborationManager Methods

#### manager.onStatusChange()

Subscribe to connection status changes.

* Signature: `manager.onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe`

#### manager.onSynced()

Subscribe to initial sync state changes.

* Signature: `manager.onSynced(callback: (synced: boolean) => void): Unsubscribe`

#### manager.initialized

Whether initialization has completed.

* Returns: `boolean`

#### manager.synced

Whether initial backend sync has completed.

* Returns: `boolean`

#### manager.status

Current connection status.

* Returns: `SyncStatus` (`'connecting' | 'connected' | 'disconnected'`)

#### manager.saveVersion()

Save a named snapshot and return its version ID.

* Signature: `manager.saveVersion(name: string): Promise<string>`

#### manager.getVersions()

List all saved versions.

* Returns: `Promise<Version[]>`

#### manager.restoreVersion()

Restore a saved version for all connected users.

* Signature: `manager.restoreVersion(versionId: string): Promise<boolean>`

#### manager.setStateFromVersion()

Apply a `Version` object's state to the shared document.

* Signature: `manager.setStateFromVersion(version: Version): Promise<void>`

#### manager.getEditor()

Get the bound Lexical editor.

* Returns: `LexicalEditor | null`

#### manager.getBinding()

Get the `@lexical/yjs` binding.

* Returns: `Binding | null`

#### manager.getLexicalProvider()

Get the provider adapter used by the Lexical binding.

* Returns: `Provider | null`

#### manager.getDoc()

Get the underlying Yjs document.

* Returns: `Y.Doc | null`

#### manager.getXmlText()

Get the shared `Y.XmlText` bound to Lexical content.

* Returns: `Y.XmlText | null`

#### manager.getXmlFragment()

Alias of `getXmlText()` provided for parity with other CRDT integrations.

* Returns: `Y.XmlText | null`

#### manager.getProvider()

Get the Velt sync provider.

* Returns: `SyncProvider | null`

#### manager.getAwareness()

Get the Yjs awareness instance.

* Returns: `Awareness | null`

#### manager.getUndoManager()

Get the collaborative Yjs undo manager.

* Returns: `Y.UndoManager | null`

#### manager.getStore()

Get the underlying Velt CRDT store.

* Returns: `Store<string> | null`

#### manager.destroy()

Destroy the binding, provider adapter, awareness state, listeners, and store. The React hooks call this automatically.

* Returns: `void`

### Custom Encryption

Encrypt CRDT data before it is stored in Velt by registering a custom encryption provider.

```tsx theme={null}
<VeltProvider
  apiKey="YOUR_API_KEY"
  authProvider={authProvider}
  encryptionProvider={{
    encrypt: async ({ data }) => myEncrypt(data),
    decrypt: async ({ data }) => myDecrypt(data),
  }}
>
  ...
</VeltProvider>
```

## Limitations

* Content written through the CRDT REST API is not currently materialized into the Lexical editor. Browser-to-REST reads work, but REST-to-browser content does not.
* Online and offline concurrent editor changes merge at the character level through the official `@lexical/yjs` binding.
