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

# Ace Editor

> Setup Multiplayer Editing for Ace Editor.

The `@veltdev/ace-crdt-react` and `@veltdev/ace-crdt` libraries connect Ace Editor to a Velt CRDT text Store backed by [Yjs](https://docs.yjs.dev/). Shared content lives in one `Y.Text`; cursors, selections, and attention highlights use transient awareness state.

## 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/ace-crdt-react @veltdev/ace-crdt react-ace ace-builds @veltdev/react @veltdev/types react react-dom
    ```
  </Tab>

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

The current base wrapper peer range targets `ace-builds@^1.44.0`. Keep one copy of `@veltdev/crdt`, `yjs`, and `y-protocols` in the final application bundle.

### Step 2: Setup Velt

Initialize Velt, authenticate a user, and set stable document identity before constructing the collaboration manager. 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 = 'shared-code-file';
    const USER = {
      userId: 'ada',
      name: 'Ada Lovelace',
      email: 'ada@example.com',
      organizationId: 'my-org-id',
    };

    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 DocumentScope() {
      const user = useCurrentUser();
      const { setDocuments } = useSetDocuments();
      const [documentReady, setDocumentReady] = useState(false);

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

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

    <VeltProvider apiKey="YOUR_VELT_API_KEY" authProvider={authProvider}>
      <DocumentScope />
    </VeltProvider>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    import { initVelt } from '@veltdev/client';

    const DOCUMENT_ID = 'shared-code-file';
    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: 'Shared Ace file',
    });

    let resolveVeltReady!: () => void;
    const veltReady = new Promise<void>((resolve) => {
      resolveVeltReady = resolve;
    });
    const initSubscription = client.getVeltInitState().subscribe((isReady) => {
      if (isReady) resolveVeltReady();
    });
    await veltReady;
    initSubscription.unsubscribe();
    ```
  </Tab>
</Tabs>

All users editing the same file must use the same Velt document ID and Ace `editorId`.

### Step 3: Create the Ace Editor

The host needs stable dimensions. Import each mode and theme before selecting it.

```html theme={null}
<div id="ace-editor" class="ace-host"></div>
```

```css theme={null}
.ace-host {
  width: 100%;
  height: 520px;
  min-height: 360px;
}
```

<Tabs>
  <Tab title="React / Next.js">
    `AceCrdtEditor` creates a `react-ace` editor. When using the hook with an editor your application owns, create Ace normally and pass the instance to the hook:

    ```tsx theme={null}
    import ace from 'ace-builds/src-noconflict/ace';
    import 'ace-builds/src-noconflict/mode-typescript';
    import 'ace-builds/src-noconflict/theme-textmate';

    const editor = ace.edit(hostElement);
    editor.setTheme('ace/theme/textmate');
    editor.session.setMode('ace/mode/typescript');
    editor.session.setUseWorker(false);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    import ace from 'ace-builds/src-noconflict/ace';
    import 'ace-builds/src-noconflict/mode-typescript';
    import 'ace-builds/src-noconflict/theme-textmate';

    const host = document.querySelector<HTMLElement>('#ace-editor')!;
    const editor = ace.edit(host);
    editor.setTheme('ace/theme/textmate');
    editor.session.setMode('ace/mode/typescript');
    editor.session.setUseWorker(false);
    editor.setOptions({
      fontSize: 14,
      tabSize: 2,
      useSoftTabs: true,
      showPrintMargin: false,
      wrap: true,
    });
    ```
  </Tab>
</Tabs>

### Step 4: Initialize Collaboration

Ace markers require a real Ace `Range`. The factory below also widens a collapsed cursor by one column so the remote caret stays visible.

<Tabs>
  <Tab title="React Component">
    ```tsx theme={null}
    import ace from 'ace-builds/src-noconflict/ace';
    import { AceCrdtEditor } from '@veltdev/ace-crdt-react';

    const Range = ace.require('ace/range').Range;

    function CollaborativeEditor() {
      return (
        <div className="ace-host">
          <AceCrdtEditor
            documentId="shared-code-file"
            mode="typescript"
            theme="textmate"
            initialContent={'export const greeting = "Hello";\n'}
            cursorData={{
              name: 'Ada Lovelace',
              email: 'ada@example.com',
              color: '#0f766e',
              colorLight: '#0f766e33',
            }}
            rangeFactory={(start, end) => {
              const visibleEnd = start.row === end.row && start.column === end.column
                ? { row: end.row, column: end.column + 1 }
                : end;
              return new Range(
                start.row,
                start.column,
                visibleEnd.row,
                visibleEnd.column,
              );
            }}
            onError={(error) => console.error('Ace collaboration:', error)}
          />
        </div>
      );
    }
    ```

    Do not pass `value` or `defaultValue` to `AceCrdtEditor`. The shared `Y.Text` is the content source of truth.
  </Tab>

  <Tab title="React Hook">
    ```tsx theme={null}
    import ace from 'ace-builds/src-noconflict/ace';
    import { useCollaboration } from '@veltdev/ace-crdt-react';

    const Range = ace.require('ace/range').Range;
    const collaboration = useCollaboration({
      editorId: 'shared-code-file',
      editor,
      initialContent: '// Start writing here\n',
      cursorData: { name: 'Ada', color: '#0f766e' },
      rangeFactory: (start, end) => {
        const visibleEnd = start.row === end.row && start.column === end.column
          ? { row: end.row, column: end.column + 1 }
          : end;
        return new Range(start.row, start.column, visibleEnd.row, visibleEnd.column);
      },
      onError: (error) => console.error('Ace collaboration:', error),
    });
    ```

    Passing `editor` creates and binds collaboration once. Do not also call `attachEditor(editor)`.

    If Ace is created after the hook renders, omit `editor` from the options and pass it through the returned callback instead:

    ```tsx theme={null}
    const collaboration = useCollaboration({
      editorId: 'shared-code-file',
      rangeFactory,
    });

    collaboration.editorRef(editor);

    // Before replacing or destroying Ace:
    collaboration.editorRef(null);
    ```
  </Tab>

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

    const Range = ace.require('ace/range').Range;
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      editor,
      initialContent: '// Start writing here\n',
      cursorData: {
        name: 'Ada Lovelace',
        email: 'ada@example.com',
        color: '#0f766e',
        colorLight: '#0f766e33',
      },
      rangeFactory: (start, end) => {
        const visibleEnd = start.row === end.row && start.column === end.column
          ? { row: end.row, column: end.column + 1 }
          : end;
        return new Range(
          start.row,
          start.column,
          visibleEnd.row,
          visibleEnd.column,
        );
      },
      onError: (error) => console.error('Ace collaboration:', error),
    });
    ```
  </Tab>
</Tabs>

#### React Context Provider (Optional)

React applications can expose one hook-managed collaboration value through context:

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

<AceCrdtProvider
  config={{
    editorId: 'shared-code-file',
    editor,
    rangeFactory,
  }}
>
  {(collaboration) => (
    <button onClick={() => collaboration.saveVersion('Checkpoint')}>
      Save version
    </button>
  )}
</AceCrdtProvider>
```

`CollaborationProvider` is an alias of `AceCrdtProvider`. Use one provider or one hook for an editor, not both.

### Step 5: Add Remote Marker Styles

The manager injects per-user marker colors and labels dynamically. Keep the editor sized and add a shared shape rule:

```css theme={null}
.ace-host {
  width: 100%;
  height: 520px;
  min-height: 360px;
}

.ace_marker-layer [class*="velt-ace-remote-marker"] {
  border-radius: 2px;
}
```

Cursor markers receive a visible two-pixel left border; selections and highlights receive a border plus a translucent background. Do not override those injected borders with `border: none`.

### Step 6: Monitor Status and Remote Selections

<Tabs>
  <Tab title="React / Next.js">
    The component render function and hook expose reactive `status`, `synced`, `remoteSelections`, and `stats` values:

    ```tsx theme={null}
    <AceCrdtEditor documentId="shared-code-file" rangeFactory={rangeFactory}>
      {(collaboration) => (
        <p>
          {collaboration.status} · {collaboration.synced ? 'Synced' : 'Syncing'}
          {' · '}{collaboration.remoteSelections.length} remote selections
        </p>
      )}
    </AceCrdtEditor>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const offStatus = manager.onStatusChange((status) => {
      document.querySelector('#status')!.textContent = status;
    });
    const offSynced = manager.onSynced((synced) => {
      document.querySelector('#synced')!.textContent = synced ? 'Yes' : 'No';
    });
    const offRemote = manager.onRemoteSelectionsChange((selections) => {
      console.log('Remote cursor, selection, and highlight state:', selections);
    });
    ```
  </Tab>
</Tabs>

Ace cursor and selection events are published automatically after attachment, and awareness changes trigger remote marker rendering. Custom UI can publish explicitly:

```ts theme={null}
manager.publishCurrentSelection('cursor');
manager.publishCurrentSelection('selection');
manager.addHighlight({
  start: { row: 0, column: 0 },
  end: { row: 0, column: 10 },
}, '#f59e0b');
```

Cursor, selection, and highlight awareness is ephemeral. It is not part of the shared file or a saved version.

### Step 7: Initial Content and Force Reset

Initial content is plain text and is applied only when the shared text is new and uninitialized:

```ts theme={null}
const initialContent = [
  'function add(a, b) {',
  '  return a + b;',
  '}',
].join('\n');
```

Use a reset only for an explicit destructive action:

```ts theme={null}
await manager.forceReset('// Reset file\n');
```

<Warning>`forceResetInitialContent` and `forceReset()` replace the shared file for every collaborator.</Warning>

### Step 8: Work with Shared Text

```ts theme={null}
const currentValue = manager.getValue();
manager.setValue('const answer = 42;', 'toolbar-set');
manager.insertText(' // note');
manager.flushEditorToStore('manual-save');
```

`insertText()` accepts an optional character index. Without one, the manager uses the current Ace cursor when available.

### Step 9: Configure Collaborative Undo and Redo

The manager creates one Yjs `UndoManager` for the shared text.

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <button onClick={() => collaboration.undo()}>Undo</button>
    <button onClick={() => collaboration.redo()}>Redo</button>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const undoManager = manager.getUndoManager();

    undoManager?.undo();
    manager.flushEditorToStore('undo');

    undoManager?.redo();
    manager.flushEditorToStore('redo');
    ```
  </Tab>
</Tabs>

Use these controls as the user-facing collaborative history. Mixing Ace's local history commands with Yjs undo can produce a history that does not match the shared document.

### Step 10: Save and Restore Versions

```ts theme={null}
const versionId = await manager.saveVersion('Before refactor');
const versions = await manager.getVersions();
await manager.restoreVersion(versionId);
```

Saving flushes Ace before the checkpoint. Restoring updates the Store and reapplies the shared text to Ace.

### Step 11: Subscribe to CRDT Events

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

    const { client } = useVeltClient();

    useEffect(() => {
      if (!client) return;
      const subscription = client.getCrdtElement()
        .on('updateData')
        .subscribe((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) => console.log('CRDT update:', event));
    ```
  </Tab>
</Tabs>

### Step 12: Attach or Replace an Editor (Alternative)

Use the attach-later lifecycle only when the Store must initialize before Ace:

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    const collaboration = useCollaboration({
      editorId: 'shared-code-file',
      initializeWithoutEditor: true,
      rangeFactory,
    });

    collaboration.attachEditor(editor, { rangeFactory });
    collaboration.detachEditor();
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    const manager = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      rangeFactory,
    });

    manager.attachEditor(editor, { rangeFactory });
    manager.detachEditor();
    ```
  </Tab>
</Tabs>

This replaces passing `editor` to `createCollaboration()`. Do not use both paths in one initialization flow.

### Step 13: Custom Encryption (Optional)

<Tabs>
  <Tab title="React / Next.js">
    ```tsx theme={null}
    <VeltProvider
      apiKey="YOUR_VELT_API_KEY"
      authProvider={authProvider}
      encryptionProvider={encryptionProvider}
    >
      <DocumentScope />
    </VeltProvider>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```ts theme={null}
    client.setEncryptionProvider(encryptionProvider);
    ```
  </Tab>
</Tabs>

See [setEncryptionProvider()](/docs/api-reference/sdk/api/api-methods#setencryptionprovider-encryptionprovider) and [VeltEncryptionProvider](/docs/api-reference/sdk/models/data-models#veltencryptionprovider).

### Step 14: Clean Up

<Tabs>
  <Tab title="React / Next.js">
    `useCollaboration()` and `AceCrdtEditor` unsubscribe and destroy their collaboration manager on unmount. When an app owns the Ace instance, use an explicit manager-first cleanup before destroying Ace:

    ```tsx theme={null}
    useEffect(() => {
      return () => {
        collaboration.destroy();
        editor.destroy();
        editor.container.remove();
      };
    }, [collaboration.destroy, editor]);
    ```

    The hook's later unmount cleanup is idempotent, so the explicit `destroy()` safely preserves the required order.
  </Tab>

  <Tab title="Other Frameworks">
    Unsubscribe first, destroy collaboration so it releases Ace listeners and markers, then destroy Ace and remove its DOM:

    ```ts theme={null}
    offStatus();
    offSynced();
    offRemote();
    crdtSubscription.unsubscribe();
    manager.destroy();
    editor.destroy();
    editor.container.remove();
    ```
  </Tab>
</Tabs>

## Notes

* **Plain-text model**: Ace modes and themes are local presentation; only text is stored in `Y.Text`.
* **One binding**: Passing `editor` to the factory attaches it automatically.
* **Range factory**: Use Ace's verified `Range` constructor for remote markers.
* **Automatic selections**: The manager attaches Ace selection listeners and publishes cursor/selection state through awareness.
* **Advanced escape hatches**: `getDoc()`, `getYText()`, `getProvider()`, `getAwareness()`, and `getUndoManager()` are for diagnostics or advanced integrations. `getText()` and `getYText()` return the same shared `Y.Text`; internally, `getYText()` delegates to `getText()`. Do not create a second Y.Doc, provider, or shared text.
* **Production authentication**: Use a server-backed React `authProvider` and never expose privileged server tokens in browser code.

## Testing and Debugging

1. Open the same Velt document and Ace `editorId` as two different authenticated users in separate browser profiles.
2. Type and delete concurrently at different positions.
3. Verify visible cursor carets, selected ranges, attention highlights, Yjs undo/redo, versions, reconnect behavior, and persistence.
4. Repeat with the production mode, theme, worker, and bundler configuration.

Common issues:

* Mode or theme is missing: import the matching `ace-builds/src-noconflict` module before creating Ace.
* Text does not sync: remove React `value`/`defaultValue` and confirm both clients use the same IDs.
* Markers do not appear: provide `cursorData`, the complete `rangeFactory`, a sized host, and marker CSS.
* Duplicate Yjs warning: deduplicate `yjs` and `@veltdev/crdt` in the application bundle.

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

## Complete Example

<Tabs>
  <Tab title="React / Next.js">
    ```tsx Complete App.tsx expandable lines theme={null}
    import { useEffect, useMemo, useState } from 'react';
    import ace from 'ace-builds/src-noconflict/ace';
    import 'ace-builds/src-noconflict/mode-typescript';
    import 'ace-builds/src-noconflict/theme-textmate';
    import {
      VeltProvider,
      useCurrentUser,
      useSetDocuments,
    } from '@veltdev/react';
    import { AceCrdtEditor } from '@veltdev/ace-crdt-react';
    import './styles.css';

    const DOCUMENT_ID = 'shared-code-file';
    const USER = {
      userId: 'ada',
      organizationId: 'my-org-id',
      name: 'Ada Lovelace',
      email: 'ada@example.com',
    };
    const Range = ace.require('ace/range').Range;

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

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

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

      return (
        <main>
          <div className="ace-host">
            <AceCrdtEditor
              documentId={DOCUMENT_ID}
              mode="typescript"
              theme="textmate"
              initialContent={'export const greeting = "Hello";\n'}
              cursorData={{
                name: user.name,
                email: user.email,
                color: '#0f766e',
                colorLight: '#0f766e33',
              }}
              rangeFactory={(start, end) => {
                const visibleEnd = start.row === end.row && start.column === end.column
                  ? { row: end.row, column: end.column + 1 }
                  : end;
                return new Range(
                  start.row,
                  start.column,
                  visibleEnd.row,
                  visibleEnd.column,
                );
              }}
              onStatusChange={(status) => console.log('Status:', status)}
              onError={console.error}
            >
              {(collaboration) => (
                <nav>
                  <button onClick={() => collaboration.undo()}>Undo</button>
                  <button onClick={() => collaboration.redo()}>Redo</button>
                  <button onClick={() => collaboration.saveVersion('Checkpoint')}>
                    Save version
                  </button>
                  <span>{collaboration.synced ? 'Synced' : collaboration.status}</span>
                </nav>
              )}
            </AceCrdtEditor>
          </div>
        </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>
      );
    }
    ```

    ```css Complete styles.css expandable lines theme={null}
    .ace-host {
      width: 100%;
      height: 520px;
      min-height: 360px;
    }

    .ace_marker-layer [class*="velt-ace-remote-marker"] {
      border-radius: 2px;
    }
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```html Complete index.html expandable lines theme={null}
    <p>Status: <span id="status">connecting</span></p>
    <p>Synced: <span id="synced">No</span></p>
    <button id="undo">Undo</button>
    <button id="redo">Redo</button>
    <button id="save-version">Save version</button>
    <div id="ace-editor" class="ace-host"></div>
    ```

    ```ts Complete app.ts expandable lines theme={null}
    import ace from 'ace-builds/src-noconflict/ace';
    import 'ace-builds/src-noconflict/mode-typescript';
    import 'ace-builds/src-noconflict/theme-textmate';
    import { initVelt } from '@veltdev/client';
    import {
      createCollaboration,
      type CollaborationManager,
    } from '@veltdev/ace-crdt';
    import './styles.css';

    const DOCUMENT_ID = 'shared-code-file';
    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: 'Shared Ace file',
    });

    let resolveVeltReady!: () => void;
    const veltReady = new Promise<void>((resolve) => {
      resolveVeltReady = resolve;
    });
    const initSubscription = client.getVeltInitState().subscribe((isReady) => {
      if (isReady) resolveVeltReady();
    });
    await veltReady;
    initSubscription.unsubscribe();

    const host = document.querySelector<HTMLElement>('#ace-editor')!;
    const editor = ace.edit(host);
    editor.setTheme('ace/theme/textmate');
    editor.session.setMode('ace/mode/typescript');
    editor.session.setUseWorker(false);

    const Range = ace.require('ace/range').Range;
    let manager: CollaborationManager | null = await createCollaboration({
      editorId: DOCUMENT_ID,
      veltClient: client,
      editor,
      initialContent: 'export const greeting = "Hello";\n',
      cursorData: {
        name: 'Ada Lovelace',
        email: 'ada@example.com',
        color: '#0f766e',
        colorLight: '#0f766e33',
      },
      rangeFactory: (start, end) => {
        const visibleEnd = start.row === end.row && start.column === end.column
          ? { row: end.row, column: end.column + 1 }
          : end;
        return new Range(
          start.row,
          start.column,
          visibleEnd.row,
          visibleEnd.column,
        );
      },
      onError: console.error,
    });

    document.querySelector('#status')!.textContent = manager.status;
    document.querySelector('#synced')!.textContent = manager.synced ? 'Yes' : 'No';

    const offStatus = manager.onStatusChange((status) => {
      document.querySelector('#status')!.textContent = status;
    });
    const offSynced = manager.onSynced((synced) => {
      document.querySelector('#synced')!.textContent = synced ? 'Yes' : 'No';
    });
    const offRemote = manager.onRemoteSelectionsChange((selections) => {
      console.log('Remote selections:', selections);
    });
    const crdtSubscription = client.getCrdtElement()
      .on('updateData')
      .subscribe((event) => console.log('CRDT update:', event));

    document.querySelector('#undo')!.addEventListener('click', () => {
      manager?.getUndoManager()?.undo();
      manager?.flushEditorToStore('undo');
    });
    document.querySelector('#redo')!.addEventListener('click', () => {
      manager?.getUndoManager()?.redo();
      manager?.flushEditorToStore('redo');
    });
    document.querySelector('#save-version')!.addEventListener('click', () => {
      void manager?.saveVersion('Manual checkpoint');
    });

    let cleanedUp = false;
    function cleanup() {
      if (cleanedUp) return;
      cleanedUp = true;
      offStatus();
      offSynced();
      offRemote();
      crdtSubscription.unsubscribe();
      manager?.destroy();
      manager = null;
      editor.destroy();
      editor.container.remove();
    }

    window.addEventListener('pagehide', cleanup, { once: true });
    ```

    ```css Complete styles.css expandable lines theme={null}
    .ace-host {
      width: 100%;
      height: 520px;
      min-height: 360px;
    }

    .ace_marker-layer [class*="velt-ace-remote-marker"] {
      border-radius: 2px;
    }
    ```
  </Tab>
</Tabs>

## How It Works

1. The app initializes Velt, authenticates the user, sets the document, and creates one Ace editor.
2. The manager creates a Velt text Store, shared `Y.Text`, provider, awareness bridge, and Yjs `UndoManager`.
3. Ace deltas are converted to character offsets and applied as Yjs insert/delete transactions.
4. Remote Yjs updates are applied back to Ace without echoing into a second local transaction.
5. Ace selection events publish awareness state; the range factory converts remote row/column data into visible Ace markers.
6. Cleanup removes subscriptions and markers before the app destroys Ace and its DOM.

## APIs

### React: AceCrdtEditor

| Prop group    | Important props                                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Collaboration | `editorId`, `documentId`, `initialContent`, `forceResetInitialContent`, `cursorData`, `rangeFactory`, `debounceMs`, `enabled` |
| Ace           | `mode`, `theme`, `name`, `width`, `height`, `setOptions`, and other React Ace props                                           |
| Lifecycle     | `onLoad`, `onEditorReady`, `onManagerReady`, `onStatusChange`, `onError`                                                      |
| UI            | `children` render function                                                                                                    |

### React: useCollaboration()

The hook returns `editorRef`, editor and manager state, shared text helpers, selection/highlight helpers, collaborative `undo()`/`redo()`, versions, statistics, attachment methods, and `destroy()`. It includes `isSynced`, `versions`, `refreshSnapshots()`, and `refreshVersions()` alongside the save and restore helpers. `value`, `remoteSelections`, and `stats` are reactive snapshots.

### CollaborationManager Methods

| Method group | Methods                                                                                                                                                   |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lifecycle    | `initialize()`, `destroy()`, `attachEditor()`, `detachEditor()`                                                                                           |
| Text         | `getValue()`, `setValue()`, `insertText()`, `flushEditorToStore()`, `forceReset()`                                                                        |
| Awareness    | `setLocalSelection()`, `publishCurrentSelection()`, `addHighlight()`, `getRemoteSelections()`, `renderRemoteSelections()`, `clearRemoteMarkers()`         |
| Status       | `initialized`, `synced`, `status`, `onStatusChange()`, `onSynced()`, `getStats()`                                                                         |
| Versions     | `saveVersion()`, `getVersions()`, `getVersionById()`, `restoreVersion()`, `setStateFromVersion()`                                                         |
| Advanced     | `getCollaborationPrimitives()`, `getDoc()`, `getText()`, `getYText()`, `getProvider()`, `getAwareness()`, `getUndoManager()`, `getEditor()`, `getStore()` |

### Exported Types and Helpers

* `AceEditorLike`, `AcePosition`, `AceSelectionRange`, `AceDelta`, `AceRangeFactory`
* `AceCursorData`, `AceAwarenessSelection`, `RemoteSelection`
* `CollaborationConfig`, `CollaborationPrimitives`, `CollaborationStats`, `BindEditorOptions`
* `normalizeText()`, `positionToIndex()`, `indexToPosition()`, `rangeToOffsets()`, `offsetsToRange()`
