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

# Monaco Editor

> Setup Multiplayer Editing for Monaco Editor.

The `@veltdev/monaco-crdt-react` and `@veltdev/monaco-crdt` libraries enable real-time collaborative editing in Monaco. The collaboration engine is built on [Yjs](https://docs.yjs.dev/), [y-monaco](https://github.com/yjs/y-monaco), 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
* Optional: TypeScript for type safety

## Setup

### Step 1: Install Dependencies

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

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

Ensure your application resolves one copy of `yjs`, `y-protocols`, and `monaco-editor`. For Vite projects, deduplicate them explicitly:

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

export default defineConfig({
  resolve: {
    dedupe: ['yjs', 'y-protocols', 'monaco-editor'],
  },
});
```

### 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">
    Wrap the app with an authenticated `VeltProvider`, then set the document after the current user is available. Keep the document hook unconditional and gate the effect:

    ```tsx theme={null}
    import { useEffect, useState } from 'react';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';

    const DOCUMENT_ID = 'code-file-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: 'Code File 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">
    Initialize one client, authenticate the user, set the document, and wait for Velt readiness before creating Monaco:

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

    const DOCUMENT_ID = 'code-file-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: 'Code File 123',
    });

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

    Every collaborator who should edit the same file must use the same Velt document ID and `editorId`.
  </Tab>
</Tabs>

### Step 3: Initialize Collaborative Editor

* Pass an `editorId` to uniquely identify the shared Monaco model.
* Do not pass React wrapper `value` or `defaultValue`, and do not seed a direct Monaco editor with non-empty local text. The CRDT-backed model owns shared content.
* Pass `initialContent` as plain text for a brand-new shared document.

<Tabs>
  <Tab title="React Component">
    `MonacoCrdtEditor` is the simplest integration. It creates Monaco, binds its model to `Y.Text`, and manages collaboration cleanup.

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

    function CollaborativeEditor() {
      return (
        <MonacoCrdtEditor
          editorId={DOCUMENT_ID}
          language="typescript"
          theme="vs-dark"
          height="500px"
          initialContent={'export const greeting = "Hello";\n'}
          cursorData={{ name: 'Ada', color: '#2563eb' }}
          options={{ minimap: { enabled: false }, wordWrap: 'on' }}
          onError={(error) => console.error('Collaboration error:', error)}
        />
      );
    }
    ```

    Normal `@monaco-editor/react` props are passed through. The wrapper owns `value`, `defaultValue`, and the internal mount handler, but still calls your `onMount` callback after capturing the editor.
  </Tab>

  <Tab title="React Hook">
    Use `useCollaboration()` with a custom Monaco setup:

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

    function CollaborativeEditor() {
      const {
        editorRef,
        primitives,
        manager,
        isLoading,
        isSynced,
        status,
        error,
      } = useCollaboration({
        editorId: DOCUMENT_ID,
        initialContent: '// Start writing here\n',
        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
            height="500px"
            language="typescript"
            onMount={(editor) => editorRef(editor)}
          />
        </div>
      );
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Create Monaco before binding collaboration. Choose exactly one binding path for each manager.

    **Automatic binding:** pass `editor` to `createCollaboration()`. The manager binds it during initialization, so do not call `bindEditor()` afterward.

    ```ts theme={null}
    import * as monaco from 'monaco-editor';
    import { createCollaboration } from '@veltdev/monaco-crdt';

    async function initializeMonaco(client) {
      const host = document.getElementById('editor');
      if (!host) throw new Error('Monaco host #editor was not found');

      const editor = monaco.editor.create(
        host,
        {
          value: '',
          language: 'typescript',
          theme: 'vs-dark',
        },
      );

      const manager = await createCollaboration({
        editorId: DOCUMENT_ID,
        veltClient: client,
        editor,
        initialContent: '// Start writing here\n',
        cursorData: { name: 'Ada', color: '#2563eb' },
        onError: (error) => console.error('Collaboration error:', error),
      });

      return { editor, manager };
    }
    ```

    **Attach later:** create Monaco, omit `editor` when creating the manager, then call `bindEditor(editor)` once. This is useful when your application deliberately separates manager creation from editor registration or later replaces the bound editor.

    ```ts theme={null}
    const host = document.getElementById('editor');
    if (!host) throw new Error('Monaco host #editor was not found');

    const editor = monaco.editor.create(
      host,
      { value: '', language: 'typescript' },
    );

    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      initialContent: '// Start writing here\n',
    });

    await manager.bindEditor(editor);
    ```

    For shared models or custom editor surfaces, `bindEditor()` also accepts the model, a set of editors, a custom awareness instance, and replacement behavior:

    ```ts theme={null}
    await manager.bindEditor(editor, {
      model: editor.getModel(),
      editors: new Set([editor, diffEditor]),
      awareness: manager.getAwareness(),
      destroyExisting: true,
    });
    ```

    Keep the defaults unless multiple Monaco surfaces intentionally share one model. Set `destroyExisting: false` only when retaining the existing binding is deliberate.

    The complete example uses automatic binding only.
  </Tab>
</Tabs>

<Warning>Do not provide React wrapper `value` or `defaultValue`, or use Monaco's local value as a second content source. A directly constructed editor can start with `value: ''`; use `initialContent` on the collaboration manager to seed shared text.</Warning>

### Step 4: Status Monitoring (Optional)

Use the hook's reactive state, component callbacks, or manager subscriptions:

<Tabs>
  <Tab title="React / Next.js">
    ```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">
    ```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>Local Monaco edits continue while disconnected and synchronize when the provider reconnects.</Note>

### Step 5: Version Management (Optional)

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

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

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

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

Restoring a version changes the shared `Y.Text` for all collaborators. Refresh the version list after each save or restore. For multi-tab interfaces, also refresh on window focus or visibility changes. If peers need immediate metadata updates, broadcast a small awareness event and refresh when they receive it.

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

`initialContent` is inserted only when the shared text is new and empty:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <MonacoCrdtEditor
      editorId={DOCUMENT_ID}
      initialContent={'function main() {\n  return true;\n}\n'}
    />
    ```

    For an explicit reset workflow:

    ```tsx theme={null}
    <MonacoCrdtEditor
      editorId={DOCUMENT_ID}
      initialContent=""
      forceResetInitialContent
    />
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Set the reset flag when creating a new manager for an explicit reset workflow:

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

<Warning>`forceResetInitialContent` clears the existing shared text. Do not enable it during normal page loads.</Warning>

### Step 7: Style Remote Cursors

`y-monaco` adds remote selection decorations to Monaco. Add base styles:

```css theme={null}
.yRemoteSelection {
  background-color: rgba(37, 99, 235, 0.2);
  opacity: 1;
}

.yRemoteSelectionHead {
  border-left: 2px solid #2563eb;
  min-height: 1.2em;
}
```

These fallback styles keep remote selections and carets visible. To apply each collaborator's awareness color and add name labels, create client-specific rules from awareness state:

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

      const awareness = manager.getAwareness();
      const doc = manager.getDoc();
      if (!awareness || !doc) return;

      const style = document.createElement('style');
      document.head.appendChild(style);

      const render = () => {
        const rules: string[] = [];
        awareness.getStates().forEach((state: any, clientId: number) => {
          if (clientId === doc.clientID || !state?.user) return;

          const color = state.user.color || '#2563eb';
          const colorLight = state.user.colorLight || `${color}33`;
          const name = String(state.user.name || 'Collaborator').replace(/"/g, '\\"');

          rules.push(`
            .yRemoteSelection-${clientId} { background-color: ${colorLight}; }
            .yRemoteSelectionHead-${clientId} { border-left: 2px solid ${color}; position: relative; }
            .yRemoteSelectionHead-${clientId}::after {
              content: "${name}";
              position: absolute;
              top: -1.55rem;
              left: -1px;
              background: ${color};
              color: white;
              padding: 2px 7px;
              border-radius: 4px;
              white-space: nowrap;
            }
          `);
        });
        style.textContent = rules.join('\n');
      };

      awareness.on('change', render);
      render();

      return () => {
        awareness.off('change', render);
        style.remove();
      };
    }, [manager]);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    function installCursorStyles(manager) {
      const awareness = manager.getAwareness();
      const doc = manager.getDoc();
      if (!awareness || !doc) return () => {};

      const style = document.createElement('style');
      document.head.appendChild(style);

      const render = () => {
        const rules = [];
        awareness.getStates().forEach((state, clientId) => {
          if (clientId === doc.clientID || !state?.user) return;

          const color = state.user.color || '#2563eb';
          const colorLight = state.user.colorLight || `${color}33`;
          const name = String(state.user.name || 'Collaborator').replace(/"/g, '\\"');

          rules.push(`
            .yRemoteSelection-${clientId} { background-color: ${colorLight}; }
            .yRemoteSelectionHead-${clientId} {
              border-left: 2px solid ${color};
              position: relative;
            }
            .yRemoteSelectionHead-${clientId}::after {
              content: "${name}";
              position: absolute;
              top: -1.55rem;
              left: -1px;
              background: ${color};
              color: white;
              padding: 2px 7px;
              border-radius: 4px;
              white-space: nowrap;
            }
          `);
        });
        style.textContent = rules.join('\n');
      };

      awareness.on('change', render);
      render();

      return () => {
        awareness.off('change', render);
        style.remove();
      };
    }

    const removeCursorStyles = installCursorStyles(manager);
    ```

    Call `removeCursorStyles()` before destroying the manager.
  </Tab>
</Tabs>

Pass local identity with `cursorData`; when omitted, the adapter falls back to the authenticated Velt user.

### Step 8: 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 9: 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">
    Configure encryption 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 10: Error Handling (Optional)

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const collab = useCollaboration({
      editorId: DOCUMENT_ID,
      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('Monaco collaboration error:', error);
      },
    });

    if (!manager.getYText() || !manager.getBinding()) {
      manager.destroy();
      editor.dispose();
      throw new Error('Monaco collaboration did not initialize');
    }
    ```
  </Tab>
</Tabs>

Manager methods return safe fallback values when collaboration resources are unavailable.

### Step 11: Access Collaboration Primitives (Advanced)

<Tabs>
  <Tab title="React / Next.js">
    The hook exposes commonly used primitives:

    ```tsx theme={null}
    const { primitives } = useCollaboration({ editorId: DOCUMENT_ID });

    const ytext = primitives?.ytext;
    const awareness = primitives?.awareness;
    const undoManager = primitives?.undoManager;
    const doc = primitives?.doc;
    const provider = primitives?.provider;
    ```
  </Tab>

  <Tab title="Other Frameworks">
    The manager exposes the same resources plus the binding, editor, and store:

    ```ts theme={null}
    const primitives = manager.getCollaborationPrimitives();
    const ytext = manager.getYText();
    const doc = manager.getDoc();
    const provider = manager.getProvider();
    const awareness = manager.getAwareness();
    const undoManager = manager.getUndoManager();
    const binding = manager.getBinding();
    const editor = manager.getEditor();
    const store = manager.getStore();
    ```
  </Tab>
</Tabs>

Use `manager.getUndoManager()?.undo()` and `redo()` for collaboration-aware history. Do not add a separate Monaco history owner for the shared model.

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

<Tabs>
  <Tab title="React / Next.js">
    Delay or disable manager creation while continuing to render Monaco:

    ```tsx theme={null}
    <MonacoCrdtEditor
      editorId={DOCUMENT_ID}
      enabled={Boolean(user && documentReady)}
    />
    ```

    The component and hook destroy the collaboration manager automatically. The hook also returns `destroy()` for early teardown. If you created Monaco yourself, dispose the editor and model separately because the manager does not own them.
  </Tab>

  <Tab title="Other Frameworks">
    Keep the disposer returned by every subscription. Destroy collaboration before disposing the Monaco editor:

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

    manager.destroy();
    editor.dispose();
    ```

    The manager destroys its `MonacoBinding`, provider, awareness listeners, and undo manager. It does not own the editor created by your application.
  </Tab>
</Tabs>

### Step 13: Client-only Rendering

Monaco depends on browser APIs. In Next.js, load the collaborative editor without server-side rendering:

Configure Monaco workers in the application bundler as well. For example, a Vite application can install the core editor worker before Monaco is created:

```ts theme={null}
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';

self.MonacoEnvironment = {
  getWorker() {
    return new editorWorker();
  },
};
```

Add language-specific workers the same way when the configured Monaco languages require them.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    import dynamic from 'next/dynamic';

    const CollaborativeMonaco = dynamic(
      () => import('./CollaborativeMonaco'),
      { ssr: false },
    );
    ```
  </Tab>

  <Tab title="Other Frameworks">
    Create Monaco and its collaboration manager only after the browser DOM exists. In an SSR application, place the imperative setup in the framework's client-mount lifecycle.
  </Tab>
</Tabs>

## Notes

* **Uncontrolled model**: Do not control the wrapper with `value` or `defaultValue`. A direct Monaco instance may start empty, but the bound `Y.Text` owns shared content.
* **One dependency instance**: Deduplicate `yjs`, `y-protocols`, and `monaco-editor` to avoid incompatible constructors and awareness instances.
* **Unique editorId**: Collaborators must use the same Velt document context and `editorId`.
* **Plain-text model**: Monaco content is stored in `Y.Text`; the Monaco `language` affects editing behavior, not shared data format.
* **Store identity**: The manager creates a `text` Store with content key `content` and source metadata `monaco`.
* **Remote cursors**: `y-monaco` supplies decorations; your CSS controls labels and colors.
* **One binding path**: Passing `editor` to `createCollaboration()` auto-binds it. If you omit `editor`, call `bindEditor(editor)` later. Never do both for the same manager.
* **Multiple editors**: Give independent models different `editorId` values. Editors sharing one model can be passed through `bindEditor()` options.
* **Other Frameworks lifecycle**: Unsubscribe events and cursor-style listeners, destroy the manager, and then dispose the Monaco editor.
* **Client-only**: Render Monaco only in the browser.
* **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 Monaco `editorId`.
3. Type, select text, undo, and redo in both windows.
4. Save and restore a version, then reload and verify persistence.

**Common issues:**

* `Yjs was already imported`: Deduplicate `yjs` and remove nested or linked duplicate copies.
* Text does not sync: Remove `value` and `defaultValue`, and confirm both clients use the same document and `editorId`.
* Editor never initializes: Confirm Monaco is mounted, Velt is ready, and a user is identified.
* Cursor labels are missing: Add the dynamic client-ID styles or supply your own awareness renderer.
* SSR errors: Load the Monaco component client-side only.

<Tip>
  Use the [VeltCrdtStoreMap debugging interface](/docs/realtime-collaboration/crdt/setup/core#veltcrdtstoremap) to inspect the shared text 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 { MonacoCrdtEditor } from '@veltdev/monaco-crdt-react';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';
    import type { CollaborationManager, SyncStatus } from '@veltdev/monaco-crdt-react';
    import './monaco-cursors.css';

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

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

      useEffect(() => {
        if (!user) {
          setDocumentReady(false);
          return;
        }
        setDocuments([{
          id: DOCUMENT_ID,
          metadata: { documentName: 'Monaco 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} | Synced: {manager?.synced ? 'Yes' : 'No'}</p>
          <input
            value={versionName}
            onChange={(event) => setVersionName(event.target.value)}
            placeholder="Version name"
          />
          <button onClick={saveVersion}>Save version</button>
          <MonacoCrdtEditor
            editorId={DOCUMENT_ID}
            language="typescript"
            theme="vs-dark"
            height="500px"
            initialContent={'export function hello() {\n  return "world";\n}\n'}
            cursorData={{ name: user.name, color: '#2563eb' }}
            options={{ minimap: { enabled: false }, wordWrap: 'on' }}
            onStatusChange={setStatus}
            onManagerReady={setManager}
            onError={console.error}
          />
        </main>
      );
    }

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

    ```css Complete monaco-cursors.css expandable lines theme={null}
    .yRemoteSelection {
      background-color: rgba(37, 99, 235, 0.2);
      opacity: 1;
    }

    .yRemoteSelectionHead {
      border-left: 2px solid #2563eb;
      min-height: 1.2em;
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    This complete example uses automatic binding: it creates Monaco first and passes that editor to `createCollaboration()`. It never calls `bindEditor()`.

    ```ts Complete app.ts expandable lines theme={null}
    import * as monaco from 'monaco-editor';
    import { initVelt } from '@veltdev/client';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/monaco-crdt';
    import './monaco-cursors.css';

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

    let resources: {
      editor: monaco.editor.IStandaloneCodeEditor;
      manager: CollaborationManager;
      offStatus: () => void;
      offSynced: () => void;
      crdtSubscription: { unsubscribe: () => void };
      removeCursorStyles: () => void;
    } | null = null;

    function installCursorStyles(manager: CollaborationManager) {
      const awareness = manager.getAwareness();
      const doc = manager.getDoc();
      if (!awareness || !doc) return () => {};

      const style = document.createElement('style');
      document.head.appendChild(style);

      const render = () => {
        const rules: string[] = [];
        awareness.getStates().forEach((state: any, clientId: number) => {
          if (clientId === doc.clientID || !state?.user) return;
          const color = state.user.color || '#2563eb';
          const colorLight = state.user.colorLight || `${color}33`;
          const name = String(state.user.name || 'Collaborator').replace(/"/g, '\\"');

          rules.push(`
            .yRemoteSelection-${clientId} { background-color: ${colorLight}; }
            .yRemoteSelectionHead-${clientId} {
              border-left: 2px solid ${color};
              position: relative;
            }
            .yRemoteSelectionHead-${clientId}::after {
              content: "${name}";
              position: absolute;
              top: -1.55rem;
              left: -1px;
              background: ${color};
              color: white;
              padding: 2px 7px;
              border-radius: 4px;
              white-space: nowrap;
            }
          `);
        });
        style.textContent = rules.join('\n');
      };

      awareness.on('change', render);
      render();

      return () => {
        awareness.off('change', render);
        style.remove();
      };
    }

    async function initializeMonaco(client) {
      const editor = monaco.editor.create(
        document.getElementById('editor')!,
        {
          value: '',
          language: 'typescript',
          theme: 'vs-dark',
          minimap: { enabled: false },
          automaticLayout: true,
        },
      );

      const manager = await createCollaboration({
        editorId: DOCUMENT_ID,
        veltClient: client,
        editor,
        initialContent: 'export function hello() {\n  return "world";\n}\n',
        cursorData: {
          name: 'Ada Lovelace',
          color: '#2563eb',
          colorLight: '#2563eb33',
        },
        onError: (error) => console.error('Collaboration error:', error),
      });

      if (!manager.getYText() || !manager.getBinding()) {
        manager.destroy();
        editor.dispose();
        throw new Error('Monaco 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 crdtSubscription = client
        .getCrdtElement()
        .on('updateData')
        .subscribe((event) => {
          if (event) console.log('CRDT update:', event);
        });
      const removeCursorStyles = installCursorStyles(manager);

      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('Before refactor');
        console.log('Saved version:', versionId, await manager.getVersions());
      });

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

    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: 'Monaco CRDT Demo',
    });

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

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

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

## How It Works

1. React applications initialize Velt with `VeltProvider`; other frameworks initialize one client with `initVelt()`. Both authenticate before setting the shared document.
2. React wrappers wait for Velt, an authenticated user, and a mounted Monaco editor. Other frameworks wait for client readiness and create Monaco before collaboration.
3. The manager creates a Velt CRDT store and obtains the shared `Y.Text`.
4. Passing `editor` to the factory auto-binds `y-monaco`; alternatively, a manager created without an editor can be bound once later with `bindEditor()`.
5. `y-monaco` maps awareness selections to editor decorations. Static CSS supplies a visible fallback caret, while dynamic rules apply each collaborator's color and label.
6. Local text transactions merge with concurrent remote transactions, and Velt persists the Yjs update stream and named versions.

## APIs

### React: MonacoCrdtEditor

| 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 context client.                            |
| `initialContent`           | `string`                       | No       | Initial plain text for a new document.                   |
| `forceResetInitialContent` | `boolean`                      | No       | Replaces existing shared text.                           |
| `cursorData`               | `MonacoCursorData`             | No       | Local cursor identity and colors.                        |
| `debounceMs`               | `number`                       | No       | Backend store debounce interval.                         |
| `enabled`                  | `boolean`                      | No       | Creates or destroys collaboration while mounted.         |
| `onError`                  | `(error: Error) => void`       | No       | Receives non-fatal errors.                               |
| `onStatusChange`           | `(status: SyncStatus) => void` | No       | Receives status changes.                                 |
| `onManagerReady`           | `(manager) => void`            | No       | Receives the initialized manager.                        |
| `onEditorReady`            | `(editor) => void`             | No       | Receives the Monaco editor.                              |
| `onMount`                  | `OnMount`                      | No       | Receives the normal Monaco mount callback after capture. |

\*Provide either `editorId` or `documentId`. Other Monaco React props are passed through except `value` and `defaultValue`.

### React: useCollaboration()

| Return                | Type                              | Description                        |
| --------------------- | --------------------------------- | ---------------------------------- |
| `editorRef`           | `(editor \| null) => void`        | Receives a Monaco editor instance. |
| `primitives`          | `CollaborationPrimitives \| null` | Shared Yjs and provider objects.   |
| `manager`             | `CollaborationManager \| null`    | Base manager.                      |
| `isLoading`           | `boolean`                         | Initialization state.              |
| `synced` / `isSynced` | `boolean`                         | Initial backend sync state.        |
| `status`              | `SyncStatus`                      | Connection status.                 |
| `error`               | `Error \| null`                   | Latest error.                      |
| `versions`            | `Version[]`                       | Saved versions.                    |
| `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/monaco-crdt';

const manager = await createCollaboration({
  editorId: DOCUMENT_ID,
  veltClient: client,
  editor,
  initialContent: '// Shared text\n',
  cursorData: { name: 'Ada', color: '#2563eb' },
  debounceMs: 250,
  onError: console.error,
});
```

This factory example uses automatic binding. For attach-later binding, omit `editor` and follow the separate alternative in Step 3; never combine the two paths.

| Config                     | Type                     | Required | Description                                                  |
| -------------------------- | ------------------------ | -------- | ------------------------------------------------------------ |
| `editorId`                 | `string`                 | Yes      | Stable key for the shared Monaco model.                      |
| `veltClient`               | `Velt`                   | Yes      | Initialized, authenticated Velt client.                      |
| `editor`                   | `IStandaloneCodeEditor`  | No       | Monaco instance to bind automatically during initialization. |
| `initialContent`           | `string`                 | No       | Plain text inserted only for a new, empty shared value.      |
| `forceResetInitialContent` | `boolean`                | No       | Explicitly replaces existing shared text.                    |
| `cursorData`               | `MonacoCursorData`       | No       | Awareness identity and cursor colors.                        |
| `debounceMs`               | `number`                 | No       | Backend persistence debounce interval.                       |
| `onError`                  | `(error: Error) => void` | No       | Receives non-fatal initialization and runtime errors.        |

### CollaborationManager Methods

| Method or property                | Description                                   |
| --------------------------------- | --------------------------------------------- |
| `initialize()`                    | Initializes the shared store.                 |
| `destroy()`                       | Destroys binding and collaboration resources. |
| `bindEditor(editor, options?)`    | Binds or rebinds a Monaco model.              |
| `getCollaborationPrimitives()`    | Returns shared primitives.                    |
| `initialized`, `synced`, `status` | Current manager state.                        |
| `onStatusChange(callback)`        | Subscribes to connection status.              |
| `onSynced(callback)`              | Subscribes to sync state.                     |
| `getDoc()`                        | Returns the `Y.Doc`.                          |
| `getText()` / `getYText()`        | Returns the shared `Y.Text`.                  |
| `getProvider()`                   | Returns the Velt sync provider.               |
| `getAwareness()`                  | Returns Yjs awareness.                        |
| `getUndoManager()`                | Returns the shared undo manager.              |
| `getBinding()`                    | Returns the `MonacoBinding`.                  |
| `getEditor()`                     | Returns the bound Monaco editor.              |
| `getStore()`                      | Returns the Velt CRDT store.                  |
| `saveVersion(name)`               | Saves a version.                              |
| `getVersions()`                   | Lists versions.                               |
| `restoreVersion(id)`              | Restores a version.                           |
| `setStateFromVersion(version)`    | Applies a supplied version object.            |

### Exported Types

* `CollaborationConfig`, `CollaborationPrimitives`, `BindEditorOptions`
* `UseCollaborationConfig` / `UseMonacoCrdtConfig`
* `UseCollaborationReturn` / `MonacoCrdtHookResult`
* `MonacoCrdtEditorProps`, `MonacoCursorData`, `MonacoBindingInstance`
* `SyncStatus`, `Unsubscribe`, `Version`
