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

# TinyMCE Editor

> Setup Multiplayer Editing for TinyMCE Editor.

Enable real-time collaborative editing on TinyMCE Editors. The collaboration editing engine is built on top of [Yjs](https://docs.yjs.dev/) 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 packages for your application:

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

    The React package provides the drop-in component and hook. The base `@veltdev/tinymce-crdt` package provides the imperative manager and content helpers.
  </Tab>

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

    The application owns TinyMCE's lifecycle. The collaboration manager owns the CRDT store, binding, awareness, and version lifecycle.
  </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 = 'tinymce-proposal';
    const USER = {
      userId: 'michael',
      name: 'Michael Scott',
      email: 'michael@example.com',
      organizationId: 'org-123',
    };

    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: 'TinyMCE proposal' },
        }]);
        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">
    ```ts theme={null}
    import { initVelt } from '@veltdev/client';

    const DOCUMENT_ID = 'tinymce-proposal';
    const client = await initVelt('YOUR_API_KEY');

    await client.setDocument(DOCUMENT_ID, {
      documentName: 'TinyMCE proposal',
    });

    await client.identify({
      userId: 'michael',
      name: 'Michael Scott',
      email: 'michael@example.com',
      organizationId: 'org-123',
    });

    const initSubscription = client.getVeltInitState().subscribe((isReady) => {
      if (isReady) void initializeTinyMCE(client);
    });
    ```

    All collaborators who should share content must use the same document and editor IDs. Create the manager only after Velt is ready and TinyMCE has returned an editor instance.
  </Tab>
</Tabs>

### Step 3: Load TinyMCE

You can load TinyMCE from your own bundle or from Tiny Cloud.

<Tabs>
  <Tab title="Self-hosted">
    Import TinyMCE, the theme, model, icons, plugins, and skin styles before rendering the editor:

    ```tsx theme={null}
    import 'tinymce/tinymce';
    import 'tinymce/icons/default';
    import 'tinymce/themes/silver';
    import 'tinymce/models/dom';
    import 'tinymce/plugins/autolink';
    import 'tinymce/plugins/link';
    import 'tinymce/plugins/lists';
    import 'tinymce/skins/ui/oxide/skin.min.css';
    ```

    Set `licenseKey="gpl"` on the editor when using TinyMCE under the GPL license.

    The default TinyMCE iframe mode uses its own content stylesheet. For self-hosted inline mode, also bundle the inline content styles and tell TinyMCE not to load skin files by URL:

    ```tsx theme={null}
    import 'tinymce/skins/ui/oxide/content.inline.min.css';

    <TinyMceCrdtEditor
      licenseKey="gpl"
      init={{
        inline: true,
        skin: false,
        content_css: false,
      }}
    />
    ```
  </Tab>

  <Tab title="Tiny Cloud">
    Pass your Tiny Cloud API key to the editor:

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

    <TinyMceCrdtEditor
      apiKey="YOUR_TINYMCE_API_KEY"
      editorId="my-tinymce-editor"
      initialContent="<p>Start writing here...</p>"
    />
    ```
  </Tab>
</Tabs>

### Step 4: Initialize Collaborative Editor

* Pass an `editorId` to uniquely identify the shared editor.
* Use `initialContent` only as the seed for a brand-new shared document.
* Do not pass `value`, `initialValue`, or `onEditorChange`; the CRDT manager owns TinyMCE content.

<Tabs>
  <Tab title="React Component">
    `TinyMceCrdtEditor` is the simplest integration. It captures TinyMCE's editor instance, waits for Velt and the current user, initializes collaboration, and cleans up automatically.

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

    function CollaborativeEditor() {
      return (
        <TinyMceCrdtEditor
          editorId="my-tinymce-editor"
          initialContent="<p>Start writing here...</p>"
          licenseKey="gpl"
          init={{
            height: 500,
            menubar: false,
            plugins: 'autolink link lists',
            toolbar: 'undo redo | blocks | bold italic | bullist numlist | link',
          }}
          cursorData={{ name: 'Ada', color: '#2563eb' }}
          onError={(error) => console.error('Collaboration error:', error)}
        />
      );
    }
    ```

    The component forwards normal `@tinymce/tinymce-react` props except `value`, `initialValue`, `onEditorChange`, and `onInit`, which are owned by the collaboration layer.
  </Tab>

  <Tab title="React Hook">
    Use `useCollaboration()` when you need to render TinyMCE yourself.

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

    function CollaborativeEditor() {
      const {
        editorRef,
        manager,
        isLoading,
        isSynced,
        status,
        error,
      } = useCollaboration({
        editorId: 'my-tinymce-editor',
        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>
          <Editor
            licenseKey="gpl"
            onInit={(_event, editor) => editorRef(editor)}
            init={{ height: 500, plugins: 'autolink link lists' }}
          />
        </div>
      );
    }
    ```

    <Warning>Keep TinyMCE uncontrolled. Do not add `value`, `initialValue`, or `onEditorChange` when using the hook.</Warning>
  </Tab>

  <Tab title="Other Frameworks">
    Use `createCollaboration()` when your application owns TinyMCE. This standalone flow includes the required Velt initialization, authentication, document setup, editor creation, manager creation, and manager-first cleanup.

    ```ts theme={null}
    import tinymce from 'tinymce/tinymce';
    import type { Editor } from 'tinymce';
    import 'tinymce/icons/default';
    import 'tinymce/themes/silver';
    import 'tinymce/models/dom';
    import 'tinymce/plugins/autolink';
    import 'tinymce/plugins/link';
    import 'tinymce/plugins/lists';
    import { initVelt } from '@veltdev/client';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/tinymce-crdt';

    const DOCUMENT_ID = 'tinymce-proposal';
    let editor: Editor | null = null;
    let manager: CollaborationManager | null = null;
    let initSubscription: { unsubscribe(): void } | null = null;

    async function main() {
      const client = await initVelt('YOUR_API_KEY');
      await client.setDocument(DOCUMENT_ID, { documentName: 'TinyMCE proposal' });

      await client.identify({
        userId: 'michael',
        name: 'Michael Scott',
        email: 'michael@example.com',
        organizationId: 'org-123',
      });

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

        [editor] = await tinymce.init({
          selector: '#editor',
          inline: true,
          license_key: 'gpl',
          plugins: 'autolink link lists',
          toolbar: 'undo redo | blocks | bold italic | forecolor backcolor | bullist numlist | link',
        });

        manager = await createCollaboration({
          editorId: DOCUMENT_ID,
          veltClient: client,
          editor,
          initialContent: '<p>Start writing here...</p>',
          debounceMs: 125,
          cursorData: { name: 'Michael Scott', color: '#2563eb' },
          onError: (error) => console.error('Collaboration error:', error),
        });
      });
    }

    function destroy() {
      initSubscription?.unsubscribe();
      manager?.destroy();
      manager = null;
      if (editor) tinymce.remove(editor);
      editor = null;
    }

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

    This example uses inline mode. The package also supports iframe-mode TinyMCE editors; omit `inline: true` to use iframe mode.
  </Tab>
</Tabs>

#### Apply Formatting Programmatically

TinyMCE formatting synchronizes as part of the shared HTML. Use TinyMCE commands for toolbar actions or custom controls:

```ts theme={null}
editor.execCommand('Bold');
editor.execCommand('Italic');
editor.execCommand('FormatBlock', false, 'h1');
editor.execCommand('InsertUnorderedList');
editor.execCommand('InsertOrderedList');
editor.execCommand('mceInsertLink', false, 'https://velt.dev');
editor.execCommand('mceApplyTextcolor', 'forecolor', '#dc2626');
editor.execCommand('mceApplyTextcolor', 'hilitecolor', '#fff59d');
```

Enable the `lists` and `link` plugins for their corresponding commands. `forecolor` maps to the text-color toolbar control, while `hilitecolor` maps to `backcolor`.

### Step 5: Status Monitoring (Optional)

<Tabs>
  <Tab title="React / Next.js">
    The hook returns reactive `status`, `isSynced`, and `error` state. The component exposes equivalent callbacks, and the manager supports event subscriptions.

    ```tsx theme={null}
    <TinyMceCrdtEditor
      editorId="my-tinymce-editor"
      onStatusChange={(status) => console.log('Status:', status)}
      onManagerReady={(manager) => {
        const offSynced = manager.onSynced((synced) => console.log('Synced:', synced));
        // Save offSynced and call it when this surrounding integration is disposed.
      }}
    />
    ```

    With the hook:

    ```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">
    Read the current manager state and retain each returned unsubscribe function:

    ```ts theme={null}
    renderStatus(manager.status, manager.synced);

    const offStatus = manager.onStatusChange((status) => {
      renderStatus(status, manager.synced);
    });

    const offSynced = manager.onSynced((synced) => {
      renderStatus(manager.status, synced);
    });

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

`onStatusChange()`, `onSynced()`, and `onRemoteCursorsChange()` immediately invoke the callback with the current value when subscribed, then invoke it again for subsequent changes. Store and call every returned unsubscribe function.

<Note>Local editing remains available while the provider reconnects. Use status for UI feedback rather than as a typing lock.</Note>

### Step 6: Version Management (Optional)

Save, list, and restore named CRDT snapshots with the hook or manager:

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

    const versionId = await saveVersion('Before major edit');
    const latestVersions = await refreshVersions();
    await restoreVersion(versionId);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const versionId = await manager.saveVersion('Before major edit');
    const versions = await manager.getVersions();
    await manager.restoreVersion(versionId);

    const suppliedVersion = versions[0];
    if (suppliedVersion) {
      await manager.setStateFromVersion(suppliedVersion);
    }
    ```

    The manager flushes the current TinyMCE HTML before saving. Restoring updates the shared state, refreshes the local editor, and syncs to other collaborators.
  </Tab>
</Tabs>

Restoring a version replaces the shared state for every collaborator. Version metadata is not itself synchronized through the editor document, so refresh it after another user saves or restores a version.

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

`initialContent` is HTML and is applied once when the shared document is empty:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <TinyMceCrdtEditor
      editorId="my-tinymce-editor"
      initialContent="<h2>Project brief</h2><p>Start collaborating...</p>"
    />
    ```

    Set `forceResetInitialContent` only for an explicit reset workflow:

    ```tsx theme={null}
    <TinyMceCrdtEditor
      editorId="my-tinymce-editor"
      initialContent="<p>Reset content</p>"
      forceResetInitialContent
    />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      editor,
      initialContent: '<h2>Project brief</h2><p>Start collaborating...</p>',
      forceResetInitialContent: false,
    });
    ```

    Set `forceResetInitialContent: true` only for a deliberate template or destructive reset workflow. Remote content otherwise wins over local initial content.
  </Tab>
</Tabs>

<Warning>`forceResetInitialContent` replaces the existing shared HTML and clears current user edits.</Warning>

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

The adapter publishes the local selection through Yjs awareness and renders remote caret and selection overlays automatically.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <TinyMceCrdtEditor
      editorId="my-tinymce-editor"
      cursorData={{
        name: user.name,
        color: '#2563eb',
        colorLight: '#2563eb33',
      }}
    />
    ```

    Render overlays into a specific element with `cursorsContainer`, or set it to `null` to keep awareness active without injected cursor DOM:

    ```tsx theme={null}
    const cursorLayer = document.getElementById('cursor-layer');

    <TinyMceCrdtEditor
      editorId="my-tinymce-editor"
      cursorsContainer={cursorLayer}
    />
    ```

    Subscribe to normalized remote cursor data for custom UI:

    ```tsx theme={null}
    useEffect(() => {
      if (!manager) return;
      return manager.onRemoteCursorsChange((cursors) => {
        console.log(cursors);
      });
    }, [manager]);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      editor,
      cursorData: {
        name: 'Michael Scott',
        color: '#2563eb',
        colorLight: '#2563eb33',
      },
      cursorsContainer: document.getElementById('cursor-layer'),
    });

    const offCursors = manager.onRemoteCursorsChange((cursors) => {
      console.log(cursors);
    });
    ```

    Pass `cursorsContainer: null` to keep awareness active without injected cursor DOM. Call `offCursors()` during teardown.
  </Tab>
</Tabs>

Override the generated cursor labels with normal CSS when needed:

```css theme={null}
.velt-tinymce-remote-cursor {
  position: absolute;
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  pointer-events: none;
}
```

### 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 TinyMCE 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)

Pass `onError` to receive non-fatal initialization, sync, recovery, and content errors:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const collab = useCollaboration({
      editorId: 'my-tinymce-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,
      veltClient: client,
      editor,
      onError: (error) => console.error('TinyMCE collaboration error:', error),
    });
    ```
  </Tab>
</Tabs>

Manager getters and version methods return safe fallback values when data is unavailable.

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

Use the manager only when you need lower-level integration points:

```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 editor = manager.getEditor();
```

You can also read or replace the canonical shared HTML:

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

### Step 13: Cleanup

<Tabs>
  <Tab title="React / Next.js">
    The React component and hook destroy the collaboration manager automatically. The hook also returns `destroy()` for early teardown.
  </Tab>

  <Tab title="Other Frameworks">
    Unsubscribe callbacks, destroy the collaboration manager, and then remove the TinyMCE editor:

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

    manager.destroy();
    tinymce.remove(editor);
    ```

    The manager does not own TinyMCE. Calling `manager.destroy()` more than once is safe, but it must release listeners and awareness before the application removes the editor.
  </Tab>
</Tabs>

## Notes

* **Uncontrolled editor**: The CRDT manager is the only owner of TinyMCE content; do not pass controlled content props.
* **Unique editorId**: Collaborators must share the same Velt document context and `editorId`.
* **HTML data model**: The adapter stores normalized HTML in a Yjs XML fragment and preserves the local selection while applying remote HTML.
* **Debounced writes**: Local editor changes are serialized after `debounceMs` (125 ms by default).
* **Remote cursors**: Cursor overlays work with iframe and inline TinyMCE modes; set `cursorsContainer={null}` for custom rendering.
* **Initial content**: Seed content is applied once unless `forceResetInitialContent` is enabled.
* **Editor modes**: The package supports both inline and iframe-mode TinyMCE editors.
* **Other Frameworks lifecycle**: Create TinyMCE before collaboration; unsubscribe listeners and destroy the manager before removing TinyMCE.
* **Production authentication**: Use Velt's recommended server-backed authentication flow; never expose privileged server tokens in browser code.

## Testing and Debugging

**To test collaboration:**

1. Log in as two unique users in separate browser profiles.
2. Open the same Velt document and TinyMCE `editorId` in both.
3. Type and format content in one window and verify it appears in the other.
4. Verify remote caret labels, selections, version restore, and persistence after reload.

**Common issues:**

* Editor not syncing: Confirm the user is identified, the Velt document is set, and both clients use the same `editorId`.
* Editor remains blank: Confirm all required self-hosted TinyMCE imports are loaded, or provide a Tiny Cloud API key.
* Local content is overwritten: Remove TinyMCE `value`, `initialValue`, and `onEditorChange` props.
* Cursors not appearing: Confirm both users are active and `cursorsContainer` was not set to `null`.
* Existing content resets: Do not enable `forceResetInitialContent` during normal loads.

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

## Complete Example

<Tabs>
  <Tab title="React / Next.js">
    ```tsx Complete App.tsx expandable lines theme={null}
    import { useEffect, useMemo, useState } from 'react';
    import { TinyMceCrdtEditor } from '@veltdev/tinymce-crdt-react';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';
    import 'tinymce/tinymce';
    import 'tinymce/icons/default';
    import 'tinymce/themes/silver';
    import 'tinymce/models/dom';
    import 'tinymce/plugins/autolink';
    import 'tinymce/plugins/link';
    import 'tinymce/plugins/lists';
    import 'tinymce/skins/ui/oxide/skin.min.css';

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

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

    function CollaborativeEditor() {
      const user = useCurrentUser();
      const { setDocuments } = useSetDocuments();
      const [documentReady, setDocumentReady] = useState(false);
      const [status, setStatus] = useState('connecting');
      const [manager, setManager] = useState<any>(null);
      const [versionName, setVersionName] = useState('');

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

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

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

      return (
        <main>
          <p>Status: {status}</p>
          <div>
            <input
              value={versionName}
              onChange={(event) => setVersionName(event.target.value)}
              placeholder="Version name"
            />
            <button onClick={saveVersion}>Save version</button>
          </div>
          <TinyMceCrdtEditor
            key={user.userId}
            editorId={DOCUMENT_ID}
            licenseKey="gpl"
            initialContent="<h2>Shared document</h2><p>Start collaborating...</p>"
            cursorData={{ name: user.name, color: '#2563eb' }}
            onStatusChange={setStatus}
            onManagerReady={setManager}
            onError={(error) => console.error(error)}
            init={{
              height: 500,
              menubar: false,
              plugins: 'autolink link lists',
              toolbar: 'undo redo | blocks | bold italic | bullist numlist | link',
            }}
          />
        </main>
      );
    }

    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_VELT_API_KEY"
          authProvider={authProvider}
        >
          <CollaborativeEditor />
        </VeltProvider>
      );
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    This vanilla TypeScript example uses TinyMCE in inline mode. It initializes TinyMCE before collaboration and destroys the manager before removing the editor.

    ```html theme={null}
    <div id="status">Connecting...</div>
    <div id="editor"></div>
    <div id="cursor-layer"></div>
    <button id="save-version">Save version</button>
    ```

    ```ts Complete main.ts expandable lines theme={null}
    import tinymce from 'tinymce/tinymce';
    import type { Editor } from 'tinymce';
    import 'tinymce/icons/default';
    import 'tinymce/themes/silver';
    import 'tinymce/models/dom';
    import 'tinymce/plugins/autolink';
    import 'tinymce/plugins/link';
    import 'tinymce/plugins/lists';
    import { initVelt } from '@veltdev/client';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/tinymce-crdt';

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

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

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

      await client.setDocument(DOCUMENT_ID, {
        documentName: 'TinyMCE proposal',
      });

      await client.identify({
        userId: 'michael',
        name: 'Michael Scott',
        email: 'michael@example.com',
        organizationId: 'my-org-id',
      });

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

        [editor] = await tinymce.init({
          selector: '#editor',
          inline: true,
          license_key: 'gpl',
          menubar: false,
          plugins: 'autolink link lists',
          toolbar: 'undo redo | blocks | bold italic | forecolor backcolor | bullist numlist | link',
        });

        manager = await createCollaboration({
          editorId: DOCUMENT_ID,
          veltClient: client,
          editor,
          initialContent: '<h2>Shared proposal</h2><p>Start collaborating...</p>',
          debounceMs: 125,
          cursorData: {
            name: 'Michael Scott',
            color: '#2563eb',
            colorLight: '#2563eb33',
          },
          cursorsContainer: document.getElementById('cursor-layer'),
          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);
        offCursors = manager.onRemoteCursorsChange((cursors) => {
          console.log('Remote cursors:', cursors);
        });

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

        const saveButton = document.getElementById('save-version');
        if (saveButton) {
          saveButton.onclick = () => {
            void manager?.saveVersion('Manual checkpoint');
          };
        }
      });
    }

    export function destroy() {
      const saveButton = document.getElementById('save-version');
      if (saveButton) saveButton.onclick = null;

      crdtSubscription?.unsubscribe();
      crdtSubscription = null;
      offCursors?.();
      offCursors = null;
      offStatus?.();
      offStatus = null;
      offSynced?.();
      offSynced = null;
      initSubscription?.unsubscribe();
      initSubscription = null;

      manager?.destroy();
      manager = null;
      if (editor) tinymce.remove(editor);
      editor = null;
    }

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

    For an iframe-mode editor, omit `inline: true`.
  </Tab>
</Tabs>

## How It Works

1. Initialize and authenticate Velt through `VeltProvider` in React or `initVelt()` and `identify()` in other frameworks, then set a stable document context.
2. Create TinyMCE before collaboration. The React component captures its editor automatically; the hook and framework-agnostic factory receive the existing instance.
3. The manager opens a Velt CRDT store keyed by `editorId` and stores normalized HTML in a Yjs XML fragment.
4. Local TinyMCE changes are debounced and written to the shared document. Remote changes update TinyMCE while preserving the local bookmark or DOM selection.
5. Yjs awareness carries user identity and selection data for remote cursor overlays in iframe and inline modes.
6. Velt persists CRDT updates and named versions. Restoring a version refreshes TinyMCE and broadcasts the restored state.
7. React wrappers destroy collaboration automatically. Other frameworks unsubscribe listeners, destroy the manager, and then remove the TinyMCE editor explicitly.

## APIs

### React: TinyMceCrdtEditor

| Prop                       | Type                           | Required | Description                                            |
| -------------------------- | ------------------------------ | -------- | ------------------------------------------------------ |
| `editorId`                 | `string`                       | No\*     | Unique key for the shared editor.                      |
| `documentId`               | `string`                       | No\*     | Used as `editorId` when `editorId` is omitted.         |
| `veltClient`               | `Velt`                         | No       | Overrides the client from `VeltProvider`.              |
| `initialContent`           | `string`                       | No       | Initial HTML for a brand-new document.                 |
| `forceResetInitialContent` | `boolean`                      | No       | Replaces shared HTML during initialization.            |
| `cursorData`               | `TinyMCECursorData`            | No       | Local cursor name and colors.                          |
| `cursorsContainer`         | `HTMLElement \| null`          | No       | Cursor overlay container; `null` disables overlay DOM. |
| `debounceMs`               | `number`                       | No       | Editor-to-CRDT write debounce; default is 125 ms.      |
| `onError`                  | `(error: Error) => void`       | No       | Receives collaboration errors.                         |
| `onStatusChange`           | `(status: SyncStatus) => void` | No       | Receives connection status changes.                    |
| `onManagerReady`           | `(manager) => void`            | No       | Receives the initialized manager.                      |
| `onEditorReady`            | `(editor) => void`             | No       | Receives the TinyMCE instance.                         |

\*Provide either `editorId` or `documentId`. Other supported TinyMCE React props are passed through, excluding the CRDT-owned content and initialization props.

### React: useCollaboration()

The hook accepts the base `CollaborationConfig` without `editor` and with an optional `veltClient`.

| Return                | Type                           | Description                                  |
| --------------------- | ------------------------------ | -------------------------------------------- |
| `editorRef`           | `(editor \| null) => void`     | Receives the TinyMCE instance from `onInit`. |
| `manager`             | `CollaborationManager \| null` | Underlying collaboration manager.            |
| `isLoading`           | `boolean`                      | `true` until initialization completes.       |
| `synced` / `isSynced` | `boolean`                      | Initial backend sync state.                  |
| `status`              | `SyncStatus`                   | Current connection status.                   |
| `error`               | `Error \| null`                | Latest collaboration error.                  |
| `versions`            | `Version[]`                    | Saved version metadata.                      |
| `saveVersion(name)`   | `Promise<string>`              | Saves a version and returns its ID.          |
| `restoreVersion(id)`  | `Promise<boolean>`             | Restores a version.                          |
| `refreshVersions()`   | `Promise<Version[]>`           | Reloads version metadata.                    |
| `destroy()`           | `void`                         | Destroys collaboration early.                |

### Other Frameworks: createCollaboration()

```tsx theme={null}
const manager = await createCollaboration({
  editorId,
  veltClient,
  editor,
  initialContent,
  cursorData,
  cursorsContainer,
  debounceMs,
  forceResetInitialContent,
  onError,
});
```

### CollaborationManager Methods

| Method or property                | Description                                              |
| --------------------------------- | -------------------------------------------------------- |
| `initialize()`                    | Initializes the store, binding, and cursor awareness.    |
| `destroy()`                       | Detaches listeners and destroys collaboration resources. |
| `initialized`                     | Whether initialization completed.                        |
| `synced`                          | Whether initial backend sync completed.                  |
| `status`                          | Current connection status.                               |
| `onStatusChange(callback)`        | Subscribes to connection status.                         |
| `onSynced(callback)`              | Subscribes to sync state.                                |
| `getHtml()`                       | Reads normalized shared HTML.                            |
| `setHtml(html)`                   | Replaces shared HTML.                                    |
| `getRemoteCursors()`              | Reads normalized remote cursor data.                     |
| `onRemoteCursorsChange(callback)` | Subscribes to remote cursor data.                        |
| `saveVersion(name)`               | Saves a named version.                                   |
| `getVersions()`                   | Lists version metadata.                                  |
| `restoreVersion(id)`              | Restores a version by ID.                                |
| `setStateFromVersion(version)`    | Applies a supplied `Version` object.                     |
| `getDoc()`                        | Returns the `Y.Doc`.                                     |
| `getXmlFragment()`                | Returns the shared `Y.XmlFragment`.                      |
| `getProvider()`                   | Returns the Velt sync provider.                          |
| `getProviderAdapter()`            | Returns the Yjs-compatible provider adapter.             |
| `getAwareness()`                  | Returns Yjs awareness.                                   |
| `getUndoManager()`                | Returns the shared `Y.UndoManager`.                      |
| `getStore()`                      | Returns the Velt CRDT store.                             |
| `getEditor()`                     | Returns the captured TinyMCE editor.                     |

### Exported Types and Helpers

* `CollaborationConfig`
* `UseCollaborationConfig` / `UseTinyMceCrdtConfig`
* `UseCollaborationReturn` / `TinyMceCrdtHookResult`
* `TinyMceCrdtEditorProps`
* `TinyMCEEditorLike`
* `TinyMCECursorData`, `TinyMCECursorRect`, `TinyMCECursorSelection`, `TinyMCERemoteCursor`
* `SyncStatus`, `Unsubscribe`, `Version`
* `normalizeHtml()`, `readHtmlFromXmlFragment()`, `writeHtmlToXmlFragment()`
* `hasSharedHtml()`, `applyInitialContent()`
