> ## 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.

# CKEditor 5 Editor

> Setup Multiplayer Editing for CKEditor 5.

The `@veltdev/ckeditor-crdt-react` and `@veltdev/ckeditor-crdt` libraries enable real-time collaborative editing in CKEditor 5. The collaboration engine is built on [Yjs](https://docs.yjs.dev/) and the 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
* A CKEditor 5 license compatible with your build
* Optional: TypeScript for type safety

## Setup

### Step 1: Install Dependencies

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

  <Tab title="Other Frameworks">
    ```bash theme={null}
    npm install @veltdev/ckeditor-crdt @veltdev/crdt @veltdev/client ckeditor5 yjs y-protocols
    ```
  </Tab>
</Tabs>

The React package provides the drop-in component and hook. The base package provides the imperative manager, provider adapter, and HTML content helpers.

```ts theme={null}
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    dedupe: ['ckeditor5', 'yjs', 'y-protocols', 'lib0'],
  },
  optimizeDeps: {
    include: ['ckeditor5'],
  },
});
```

### 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 = 'article-123';
    const USER = {
      userId: 'user-1',
      name: 'Ada Lovelace',
      email: 'ada@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: 'Article 123' },
        }]);
        setDocumentReady(true);
      }, [user, setDocuments]);

      return user && documentReady
        ? <CollaborativeEditor />
        : <p>Preparing collaboration...</p>;
    }

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

  <Tab title="Other Frameworks">
    Authenticate before setting the document. Wait for Velt readiness before creating CKEditor:

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

    const DOCUMENT_ID = 'article-123';
    const client = await initVelt('YOUR_API_KEY');

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

    await client.setDocument(DOCUMENT_ID, {
      documentName: 'Article 123',
    });

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

### Step 3: Configure CKEditor

Import the editor constructor, the features your build needs, and CKEditor styles:

```tsx theme={null}
import {
  ClassicEditor,
  Essentials,
  Paragraph,
  Bold,
  Italic,
  Underline,
  Heading,
  List,
  Highlight,
  FontColor,
  FontBackgroundColor,
  Undo,
  GeneralHtmlSupport,
} from 'ckeditor5';
import 'ckeditor5/ckeditor5.css';

const editorConfig = {
  licenseKey: 'GPL',
  plugins: [
    Essentials,
    Paragraph,
    Bold,
    Italic,
    Underline,
    Heading,
    List,
    Highlight,
    FontColor,
    FontBackgroundColor,
    Undo,
    GeneralHtmlSupport,
  ],
  toolbar: [
    'undo', 'redo', '|',
    'heading', '|',
    'bold', 'italic', 'underline', '|',
    'fontColor', 'fontBackgroundColor', 'highlight', '|',
    'bulletedList', 'numberedList',
  ],
};
```

Use your commercial CKEditor license key instead of `GPL` when required by your distribution.

CKEditor serializes highlights as `<mark>` elements. This markup persists through realtime sync, force reset, and version restore:

```html theme={null}
<mark class="marker-yellow" style="background-color:#fff59d;">Highlighted</mark>
```

### Step 4: Initialize Collaborative Editor

* Pass an `editorId` to uniquely identify the shared editor.
* Do not pass CKEditor React's `data` or `onReady` to the drop-in component; the CRDT wrapper owns content and editor capture.
* Use `initialContent` as HTML seed content for a new shared document.

The wrapper registers its Yjs fragment observer before the Store loads remote state. This ordering is required: attaching the observer after `Store.initialize()` can miss the initial server hydration.

<Tabs>
  <Tab title="React Component">
    ```tsx theme={null}
    import { CKEditorCrdtEditor } from '@veltdev/ckeditor-crdt-react';

    function CollaborativeEditor() {
      return (
        <CKEditorCrdtEditor
          editor={ClassicEditor}
          editorId={DOCUMENT_ID}
          initialContent="<p>Start writing here...</p>"
          config={editorConfig}
          cursorData={{ name: 'Ada', color: '#0f766e' }}
          debounceMs={125}
          onError={(error) => console.error('Collaboration error:', error)}
        />
      );
    }
    ```

    Standard CKEditor React props pass through except `data` and `onReady`. Use `onEditorReady` to receive the captured editor and `onCKEditorError` for CKEditor watchdog errors.
  </Tab>

  <Tab title="React Hook">
    Use `useCollaboration()` when you render CKEditor yourself:

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

    function CollaborativeEditor() {
      const {
        editorRef,
        manager,
        primitives,
        isLoading,
        isSynced,
        status,
        error,
      } = useCollaboration({
        editorId: DOCUMENT_ID,
        initialContent: '<p>Start writing here...</p>',
        onError: (err) => console.error('Collaboration error:', err),
      });

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

      return (
        <div>
          <div>Status: {isLoading ? 'loading' : status} | Synced: {isSynced ? 'Yes' : 'No'}</div>
          <CKEditor
            editor={ClassicEditor}
            data=""
            config={editorConfig}
            onReady={(editor) => editorRef(editor)}
            onAfterDestroy={() => editorRef(null)}
          />
        </div>
      );
    }
    ```

    <Warning>When using the hook, keep `data` empty and do not update it from React state. Forward `onAfterDestroy` to `editorRef(null)` so the manager releases a destroyed editor instance.</Warning>
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    import { createCollaboration } from '@veltdev/ckeditor-crdt';

    async function initializeCKEditor(client) {
      const editor = await ClassicEditor.create(
        document.querySelector('#editor')!,
        editorConfig,
      );

      const manager = await createCollaboration({
        editorId: DOCUMENT_ID,
        veltClient: client,
        editor,
        initialContent: '<p>Start writing here...</p>',
        debounceMs: 125,
        cursorData: { name: 'Ada', color: '#0f766e' },
        cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
        onError: (error) => console.error('Collaboration error:', error),
      });

      return { editor, manager };
    }
    ```
  </Tab>
</Tabs>

Set the Velt document context before rendering the editor. The React integration explicitly waits until Velt is initialized, a user is authenticated, the CKEditor instance is ready, and `enabled` is not `false`. Document context is a prerequisite, but the hook does not explicitly wait for it.

### Step 5: Status Monitoring (Optional)

The hook returns reactive `status`, `synced` / `isSynced`, `isLoading`, and `error` state. The component offers `onStatusChange` and `onManagerReady` callbacks.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    useEffect(() => {
      if (!manager) return;

      const offStatus = manager.onStatusChange(console.log);
      const offSynced = manager.onSynced(console.log);

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

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const offStatus = manager.onStatusChange((status) => {
      console.log('Status:', status);
    });
    const offSynced = manager.onSynced((synced) => {
      console.log('Synced:', synced);
    });

    // Cleanup:
    offStatus();
    offSynced();
    ```
  </Tab>
</Tabs>

<Note>Do not disable local editing simply because the provider is connecting. Local HTML changes synchronize when the connection returns.</Note>

### Step 6: Version Management (Optional)

The hook keeps version metadata in React state and refreshes it after saves and restores:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const {
      versions,
      saveVersion,
      restoreVersion,
      refreshVersions,
    } = useCollaboration({ editorId: DOCUMENT_ID });

    const versionId = await saveVersion('Approved copy');
    await refreshVersions();
    await restoreVersion(versionId);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const versionId = await manager.saveVersion('Approved copy');
    let versions = await manager.getVersions();

    await manager.restoreVersion(versionId);
    versions = await manager.getVersions();
    ```
  </Tab>
</Tabs>

The methods are no-throw: when unavailable they resolve to `''`, `false`, or `[]`. A restore updates the shared HTML for every collaborator.

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

`initialContent` is normalized HTML and is applied once to a new empty shared document:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <CKEditorCrdtEditor
      editor={ClassicEditor}
      editorId={DOCUMENT_ID}
      config={editorConfig}
      initialContent="<h2>Project brief</h2><p>Start collaborating...</p>"
    />
    ```

    For an explicit reset:

    ```tsx theme={null}
    <CKEditorCrdtEditor
      editor={ClassicEditor}
      editorId={DOCUMENT_ID}
      config={editorConfig}
      initialContent="<p>Fresh template</p>"
      forceResetInitialContent
    />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Add the reset options to the manager creation call from Step 4:

    ```ts theme={null}
    const editor = await ClassicEditor.create(
      document.querySelector('#editor')!,
      editorConfig,
    );

    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      editor,
      initialContent: '<p>Fresh template</p>',
      forceResetInitialContent: true,
    });
    ```
  </Tab>
</Tabs>

<Warning>`forceResetInitialContent` replaces existing shared HTML. Do not enable it during normal initialization.</Warning>

### Step 8: Configure Remote Cursors (Optional)

Remote carets and selections render in an overlay anchored to CKEditor's editable surface.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <CKEditorCrdtEditor
      editor={ClassicEditor}
      editorId={DOCUMENT_ID}
      config={editorConfig}
      cursorData={{
        name: user.name,
        color: '#2563eb',
        colorLight: '#2563eb33',
      }}
    />
    ```

    Pass a custom `cursorsContainer`, or set it to `null` to disable overlay DOM while retaining awareness data.

    The hook also returns reactive `remoteCursors`:

    ```tsx theme={null}
    const { remoteCursors } = useCollaboration({
      editorId: DOCUMENT_ID,
      cursorData: { name: user.name, color: '#2563eb' },
    });

    return (
      <ul>
        {remoteCursors.map((cursor) => (
          <li key={cursor.clientId} style={{ color: cursor.color }}>
            {cursor.name}
          </li>
        ))}
      </ul>
    );
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Pass cursor identity and an optional overlay container when creating the manager:

    ```ts theme={null}
    const editor = await ClassicEditor.create(
      document.querySelector('#editor')!,
      editorConfig,
    );

    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      editor,
      cursorData: {
        name: 'Ada Lovelace',
        color: '#2563eb',
        colorLight: '#2563eb33',
      },
      cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
    });

    const offRemoteCursors = manager.onRemoteCursorsChange((cursors) => {
      console.log('Remote cursors:', cursors);
    });
    ```

    Set `cursorsContainer: null` to keep awareness data without rendering the overlay. Call `offRemoteCursors()` during cleanup.
  </Tab>
</Tabs>

Optional host styling:

```css theme={null}
.velt-ckeditor-cursor-overlay {
  pointer-events: none;
}

.velt-ckeditor-remote-selection {
  border-radius: 2px;
  mix-blend-mode: multiply;
  pointer-events: none;
  position: absolute;
}

.velt-ckeditor-remote-cursor,
.velt-ckeditor-remote-cursor-caret {
  border-radius: 999px;
  min-height: 18px;
  pointer-events: none;
  position: absolute;
  width: 2px;
}

.velt-ckeditor-remote-cursor-label {
  background: white;
  border: 1px solid;
  border-left-width: 4px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 700;
  padding: 3px 8px;
  pointer-events: none;
  position: absolute;
  white-space: nowrap;
}

```

The built-in overlay creates `.velt-ckeditor-remote-cursor`, `.velt-ckeditor-remote-cursor-caret`, `.velt-ckeditor-remote-cursor-label`, and `.velt-ckeditor-remote-selection` elements. Cursor labels also carry the collaborator name in `data-velt-cursor-name` for selectors, tests, or custom styling.

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

<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);
      });

    // Cleanup:
    crdtSubscription.unsubscribe();
    ```
  </Tab>
</Tabs>

### Step 10: Custom Encryption (Optional)

<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">
    Set encryption before creating CKEditor collaboration:

    ```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)

Use `onError` for collaboration errors and `onCKEditorError` for official CKEditor React watchdog errors:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <CKEditorCrdtEditor
      editor={ClassicEditor}
      editorId={DOCUMENT_ID}
      config={editorConfig}
      onError={(error) => console.error('Collaboration:', error)}
      onCKEditorError={(error, details) => console.error('CKEditor:', error, details)}
    />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    The direct manager reports collaboration errors through `onError`. Handle CKEditor creation errors separately:

    ```ts theme={null}
    try {
      const editor = await ClassicEditor.create(
        document.querySelector('#editor')!,
        editorConfig,
      );

      const manager = await createCollaboration({
        editorId: DOCUMENT_ID,
        veltClient: client,
        editor,
        onError: (error) => {
          console.error('CKEditor collaboration error:', error);
        },
      });
    } catch (error) {
      console.error('CKEditor creation failed:', error);
    }
    ```
  </Tab>
</Tabs>

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

The hook's `primitives` object groups lower-level resources:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const { primitives } = useCollaboration({ editorId: DOCUMENT_ID });

    const editor = primitives?.editor;
    const doc = primitives?.doc;
    const xmlFragment = primitives?.xmlFragment;
    const provider = primitives?.provider;
    const providerAdapter = primitives?.providerAdapter;
    const awareness = primitives?.awareness;
    const undoManager = primitives?.undoManager;
    const store = primitives?.store;
    ```

    Read or replace normalized shared HTML with the manager:

    ```tsx theme={null}
    const html = manager.getHtml();
    manager.setHtml('<p>Replacement content</p>');
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const doc = manager.getDoc();
    const xmlFragment = manager.getXmlFragment();
    const provider = manager.getProvider();
    const providerAdapter = manager.getProviderAdapter();
    const awareness = manager.getAwareness();
    const undoManager = manager.getUndoManager();
    const store = manager.getStore();

    const html = manager.getHtml();
    manager.setHtml('<p>Replacement content</p>');

    // Collaboration-aware application controls:
    undoManager?.undo();
    undoManager?.redo();
    ```
  </Tab>
</Tabs>

The CKEditor source build includes its `Undo` plugin for the native toolbar. For custom collaboration-aware controls, use the manager's Yjs `UndoManager`; do not trigger both history paths from the same action.

### Step 13: Enable, Disable, and Cleanup

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const collaboration = useCollaboration({
      editorId: DOCUMENT_ID,
      enabled: Boolean(user && documentReady),
    });
    ```

    Changing `enabled` to `false` destroys the current manager; changing it back reinitializes collaboration. The component and hook also clean up automatically on unmount or dependency changes, and the hook exposes `destroy()` for early teardown.

    If you render CKEditor yourself, the editor remains application-owned. Forward `onAfterDestroy` to `editorRef(null)` and destroy CKEditor according to its normal lifecycle.
  </Tab>

  <Tab title="Other Frameworks">
    Keep every disposer. Destroy the manager before destroying the application-owned CKEditor instance:

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

    manager.destroy();
    await editor.destroy();
    ```
  </Tab>
</Tabs>

## Notes

* **Uncontrolled content**: The CRDT manager owns CKEditor data; do not synchronize it through React state.
* **CKEditor configuration**: Include every plugin used by the toolbar and provide the correct CKEditor license key.
* **Unique editorId**: Collaborators must share the same Velt document context and editor ID.
* **HTML data model**: The adapter stores normalized HTML in a Yjs XML fragment and preserves local selection while applying remote content.
* **Initial remote hydration**: The manager registers its Yjs fragment observer before `Store.initialize()`. Preserve that ordering in custom integrations so server state loaded during initialization is not missed.
* **Remote cursors**: The adapter renders an overlay and also exposes normalized cursor state for custom UI.
* **StrictMode-safe**: The hook destroys stale managers during React remounts and dependency changes.
* **React readiness**: The hook waits for Velt initialization, an authenticated user, the CKEditor instance, and `enabled`. It does not explicitly wait for document context, so set the document first.
* **Editor-first direct setup**: Other frameworks create CKEditor before creating the collaboration manager.
* **Other Frameworks lifecycle**: Unsubscribe callbacks, destroy the manager, and then destroy the application-owned CKEditor instance.
* **One dependency instance**: Deduplicate `ckeditor5`, `yjs`, `y-protocols`, and `lib0` in linked or monorepo builds.
* **Initial content**: Seed HTML is applied once unless an explicit reset is requested.
* **Production authentication**: Use Velt's recommended server-backed authentication flow.

## Testing and Debugging

**To test collaboration:**

1. Log in as two unique users in separate browser profiles.
2. Open the same Velt document and CKEditor `editorId`.
3. Edit text, headings, marks, and lists in both windows.
4. Verify remote selections, version restore, reconnection, and persistence after reload.

**Common issues:**

* Toolbar item unavailable: Add its CKEditor plugin to `config.plugins`.
* License error: Provide `GPL` only for a GPL-compatible use, otherwise use your commercial license key.
* Content does not sync: Remove controlled `data` updates and confirm both clients use the same document and `editorId`.
* Manager never appears: Confirm the document context was set first, Velt is initialized, a user is identified, and CKEditor's `onReady` fired. The document is a prerequisite, not an explicit hook wait condition.
* Cursors not visible: Ensure `cursorsContainer` is not `null` and the editable element is mounted.
* Stale manager after watchdog restart: Forward `onAfterDestroy` when using the hook directly.

<Tip>
  Use the [VeltCrdtStoreMap debugging interface](/docs/realtime-collaboration/crdt/setup/core#veltcrdtstoremap) to inspect the shared XML and provider state.
</Tip>

## Complete Example

<Tabs>
  <Tab title="React / Next.js">
    ```tsx Complete App.tsx expandable lines theme={null}
    import { useEffect, useState } from 'react';
    import { CKEditorCrdtEditor } from '@veltdev/ckeditor-crdt-react';
    import type { CollaborationManager, SyncStatus } from '@veltdev/ckeditor-crdt-react';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';
    import {
      ClassicEditor,
      Essentials,
      Paragraph,
      Bold,
      Italic,
      Underline,
      Heading,
      List,
      Highlight,
      FontColor,
      FontBackgroundColor,
      Undo,
      GeneralHtmlSupport,
    } from 'ckeditor5';
    import 'ckeditor5/ckeditor5.css';

    const DOCUMENT_ID = 'ckeditor-crdt-demo-doc-1';

    const editorConfig = {
      licenseKey: 'GPL',
      plugins: [
        Essentials,
        Paragraph,
        Bold,
        Italic,
        Underline,
        Heading,
        List,
        Highlight,
        FontColor,
        FontBackgroundColor,
        Undo,
        GeneralHtmlSupport,
      ],
      toolbar: [
        'undo', 'redo', '|',
        'heading', '|',
        'bold', 'italic', 'underline', '|',
        'fontColor', 'fontBackgroundColor', 'highlight', '|',
        'bulletedList', 'numberedList',
      ],
    };

    const USER = {
      userId: 'ada',
      name: 'Ada Lovelace',
      email: 'ada@example.com',
      organizationId: 'my-org-id',
      color: '#2563eb',
    };

    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 CollaborativeEditor() {
      const user = useCurrentUser();
      const [status, setStatus] = useState<SyncStatus>('connecting');
      const [manager, setManager] = useState<CollaborationManager | null>(null);
      const [versionName, setVersionName] = useState('');
      const { setDocuments } = useSetDocuments();
      const [documentReady, setDocumentReady] = useState(false);

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

      if (!user || !documentReady) return <p>Preparing collaboration...</p>;

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

      return (
        <main>
          <p>Status: {status} | Synced: {manager?.synced ? 'Yes' : 'No'}</p>
          <input
            value={versionName}
            onChange={(event) => setVersionName(event.target.value)}
            placeholder="Version name"
          />
          <button onClick={saveVersion}>Save version</button>
          <CKEditorCrdtEditor
            editor={ClassicEditor}
            editorId={DOCUMENT_ID}
            config={editorConfig}
            initialContent="<h2>Shared document</h2><p>Start collaborating...</p>"
            cursorData={{ name: user.name, color: '#2563eb' }}
            onStatusChange={setStatus}
            onManagerReady={setManager}
            onError={console.error}
          />
        </main>
      );
    }

    export default function App() {
      return (
        <VeltProvider
          apiKey="YOUR_VELT_API_KEY"
          authProvider={authProvider}
        >
          <CollaborativeEditor />
        </VeltProvider>
      );
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Create the CKEditor instance first. Only after it resolves should you pass that exact instance to the framework-agnostic collaboration manager.

    ```ts Complete app.ts expandable lines theme={null}
    import { initVelt } from '@veltdev/client';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/ckeditor-crdt';
    import {
      ClassicEditor,
      Essentials,
      Paragraph,
      Bold,
      Italic,
      Underline,
      Heading,
      List,
      Highlight,
      FontColor,
      FontBackgroundColor,
      Undo,
      GeneralHtmlSupport,
    } from 'ckeditor5';
    import 'ckeditor5/ckeditor5.css';

    const DOCUMENT_ID = 'ckeditor-crdt-demo-doc-1';

    const editorConfig = {
      licenseKey: 'GPL',
      plugins: [
        Essentials,
        Paragraph,
        Bold,
        Italic,
        Underline,
        Heading,
        List,
        Highlight,
        FontColor,
        FontBackgroundColor,
        Undo,
        GeneralHtmlSupport,
      ],
      toolbar: [
        'undo', 'redo', '|',
        'heading', '|',
        'bold', 'italic', 'underline', '|',
        'fontColor', 'fontBackgroundColor', 'highlight', '|',
        'bulletedList', 'numberedList',
      ],
    };

    let resources: {
      editor: Awaited<ReturnType<typeof ClassicEditor.create>>;
      manager: CollaborationManager;
      offStatus: () => void;
      offSynced: () => void;
      offRemoteCursors: () => void;
      crdtSubscription: { unsubscribe: () => void };
    } | null = null;

    async function initializeCKEditor(client) {
      const editor = await ClassicEditor.create(
        document.querySelector('#editor')!,
        editorConfig,
      );

      const manager = await createCollaboration({
        editorId: DOCUMENT_ID,
        veltClient: client,
        editor,
        initialContent: '<h2>Shared document</h2><p>Start collaborating...</p>',
        debounceMs: 125,
        cursorData: {
          name: 'Ada Lovelace',
          color: '#2563eb',
          colorLight: '#2563eb33',
        },
        cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
        onError: (error) => console.error('Collaboration error:', error),
      });

      if (!manager.getXmlFragment() || !manager.getEditor()) {
        manager.destroy();
        await editor.destroy();
        throw new Error('CKEditor collaboration did not initialize');
      }

      const offStatus = manager.onStatusChange((status) => {
        document.getElementById('status')!.textContent = status;
      });
      const offSynced = manager.onSynced((synced) => {
        document.getElementById('synced')!.textContent = synced ? 'Yes' : 'No';
      });
      const offRemoteCursors = manager.onRemoteCursorsChange((cursors) => {
        console.log('Remote cursors:', cursors);
      });
      const crdtSubscription = client
        .getCrdtElement()
        .on('updateData')
        .subscribe((event) => {
          if (event) console.log('CRDT update:', event);
        });

      document.getElementById('undo')?.addEventListener('click', () => {
        manager.getUndoManager()?.undo();
      });
      document.getElementById('redo')?.addEventListener('click', () => {
        manager.getUndoManager()?.redo();
      });
      document.getElementById('save-version')?.addEventListener('click', async () => {
        const versionId = await manager.saveVersion('Approved copy');
        console.log('Saved version:', versionId, await manager.getVersions());
      });

      resources = {
        editor,
        manager,
        offStatus,
        offSynced,
        offRemoteCursors,
        crdtSubscription,
      };
    }

    const client = await initVelt('YOUR_VELT_API_KEY');

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

    await client.setDocument(DOCUMENT_ID, {
      documentName: 'CKEditor CRDT Demo',
    });

    let initializationStarted = false;
    const initSubscription = client.getVeltInitState().subscribe((isReady) => {
      if (!isReady || initializationStarted) return;
      initializationStarted = true;
      void initializeCKEditor(client);
    });

    window.addEventListener('beforeunload', () => {
      resources?.crdtSubscription.unsubscribe();
      resources?.offRemoteCursors();
      resources?.offStatus();
      resources?.offSynced();
      initSubscription.unsubscribe();

      resources?.manager.destroy();
      void resources?.editor.destroy();
    });
    ```
  </Tab>
</Tabs>

## How It Works

1. React applications initialize Velt through an authenticated `VeltProvider`; other frameworks use `initVelt()`. Both authenticate before setting the shared document.
2. The React hook explicitly waits for Velt initialization, an authenticated user, the CKEditor instance, and `enabled`. The document remains a prerequisite but is not an explicit hook wait condition.
3. Direct integrations create CKEditor first and pass the resolved editor instance to `createCollaboration()`.
4. The manager opens a Velt CRDT store keyed by `editorId`, registers the fragment observer, and only then initializes the Store so initial remote HTML is observed.
5. CKEditor model changes are debounced into shared transactions; remote changes update CKEditor while preserving the local selection when possible.
6. Awareness carries user and selection metadata for remote cursor overlays, and Velt persists CRDT updates and named versions.

## APIs

### React: CKEditorCrdtEditor

| Prop                               | Type                           | Required | Description                                           |
| ---------------------------------- | ------------------------------ | -------- | ----------------------------------------------------- |
| `editor`                           | `unknown`                      | Yes      | CKEditor constructor, usually `ClassicEditor`.        |
| `editorId`                         | `string`                       | No\*     | Unique key for the shared editor.                     |
| `documentId`                       | `string`                       | No\*     | Used as `editorId` when omitted.                      |
| `config`                           | `Record<string, unknown>`      | No       | CKEditor plugins, toolbar, and license configuration. |
| `veltClient`                       | `Velt`                         | No       | Overrides the context client.                         |
| `initialContent`                   | `string`                       | No       | Initial HTML for a new document.                      |
| `forceResetInitialContent`         | `boolean`                      | No       | Replaces shared HTML during initialization.           |
| `cursorData`                       | `CKEditorCursorData`           | No       | Local cursor identity and colors.                     |
| `cursorsContainer`                 | `HTMLElement \| null`          | No       | Overlay container; `null` disables overlay DOM.       |
| `debounceMs`                       | `number`                       | No       | Editor-to-CRDT write debounce.                        |
| `onError`                          | `(error: Error) => void`       | No       | Collaboration errors.                                 |
| `onCKEditorError`                  | `function`                     | No       | CKEditor React watchdog errors.                       |
| `onStatusChange`                   | `(status: SyncStatus) => void` | No       | Status callback.                                      |
| `onManagerReady` / `onEditorReady` | `function`                     | No       | Receives manager or editor.                           |

\*Provide either `editorId` or `documentId`. Other CKEditor React props pass through except CRDT-owned `data` and `onReady`.

### React: useCollaboration()

| Return                | Type                                      | Description                        |
| --------------------- | ----------------------------------------- | ---------------------------------- |
| `editorRef`           | `(editor \| null) => void`                | Captures or releases CKEditor.     |
| `manager`             | `CollaborationManager \| null`            | Base manager.                      |
| `primitives`          | `CKEditorCollaborationPrimitives \| null` | CKEditor, Yjs, and Velt resources. |
| `isLoading`           | `boolean`                                 | Initialization state.              |
| `synced` / `isSynced` | `boolean`                                 | Initial backend sync state.        |
| `status`              | `SyncStatus`                              | Connection status.                 |
| `error`               | `Error \| null`                           | Latest error.                      |
| `versions`            | `Version[]`                               | Saved versions.                    |
| `remoteCursors`       | `CKEditorRemoteCursor[]`                  | Reactive remote cursor metadata.   |
| `saveVersion(name)`   | `Promise<string>`                         | Saves a version.                   |
| `restoreVersion(id)`  | `Promise<boolean>`                        | Restores a version.                |
| `refreshVersions()`   | `Promise<Version[]>`                      | Reloads versions.                  |
| `destroy()`           | `void`                                    | Destroys collaboration early.      |

### Other Frameworks: createCollaboration()

```ts theme={null}
import { createCollaboration } from '@veltdev/ckeditor-crdt';

const editor = await ClassicEditor.create(
  document.querySelector('#editor')!,
  editorConfig,
);

const manager = await createCollaboration({
  editorId: DOCUMENT_ID,
  veltClient: client,
  editor,
  initialContent: '<p>Start writing here...</p>',
  debounceMs: 125,
  cursorData: { name: 'Ada', color: '#0f766e' },
  cursorsContainer: document.querySelector<HTMLElement>('#editor-surface'),
  onError: console.error,
});
```

| Config                     | Type                     | Required | Description                                                     |
| -------------------------- | ------------------------ | -------- | --------------------------------------------------------------- |
| `editorId`                 | `string`                 | Yes      | Stable key for the shared CKEditor document.                    |
| `veltClient`               | `Velt`                   | Yes      | Initialized, authenticated Velt client with a current document. |
| `editor`                   | `CKEditorEditorLike`     | Yes      | CKEditor instance created before the manager.                   |
| `initialContent`           | `string`                 | No       | Normalized HTML seed for a new, empty shared document.          |
| `forceResetInitialContent` | `boolean`                | No       | Explicitly replaces existing shared HTML.                       |
| `cursorData`               | `CKEditorCursorData`     | No       | Awareness identity and cursor colors.                           |
| `cursorsContainer`         | `HTMLElement \| null`    | No       | Cursor overlay host; `null` disables overlay DOM.               |
| `debounceMs`               | `number`                 | No       | Editor-to-CRDT write debounce.                                  |
| `onError`                  | `(error: Error) => void` | No       | Receives non-fatal initialization and runtime errors.           |

### CollaborationManager Methods

| Method or property                                    | Description                               |
| ----------------------------------------------------- | ----------------------------------------- |
| `initialize()` / `destroy()`                          | Starts or destroys collaboration.         |
| `initialized`, `synced`, `status`                     | Current manager state.                    |
| `onStatusChange(callback)` / `onSynced(callback)`     | Status subscriptions.                     |
| `getHtml()` / `setHtml(html)`                         | Reads or replaces normalized HTML.        |
| `getRemoteCursors()`                                  | Reads remote cursor metadata.             |
| `onRemoteCursorsChange(callback)`                     | Subscribes to cursor metadata.            |
| `getDoc()` / `getXmlFragment()`                       | Returns shared Yjs data.                  |
| `getProvider()` / `getProviderAdapter()`              | Returns provider objects.                 |
| `getAwareness()` / `getUndoManager()`                 | Returns awareness and undo manager.       |
| `getStore()` / `getEditor()`                          | Returns Velt store and CKEditor instance. |
| `saveVersion(name)` / `getVersions()`                 | Saves or lists versions.                  |
| `restoreVersion(id)` / `setStateFromVersion(version)` | Restores shared state.                    |

### Exported Types and Helpers

* `CollaborationConfig`, `UseCollaborationConfig` / `UseCKEditorCrdtConfig`
* `UseCollaborationReturn` / `CKEditorCrdtHookResult`
* `CKEditorCrdtEditorProps`, `CKEditorCollaborationPrimitives`, `CKEditorEditorLike`
* `CKEditorCursorData`, `CKEditorCursorRect`, `CKEditorCursorSelection`, `CKEditorRemoteCursor`
* `SyncStatus`, `Unsubscribe`, `Version`
* `normalizeHtml()`, `readHtmlFromXmlFragment()`, `writeHtmlToXmlFragment()`
* `hasSharedHtml()`, `applyInitialContent()`
