export const OperationsTable = ({ children }) => (
  <div className="overflow-hidden rounded-[22px] not-prose">
    <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
      <thead className="bg-gray-100 dark:bg-white/10">
        <tr>
          <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Type</th>
          <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Description</th>
          <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Example</th>
        </tr>
      </thead>
      <tbody className="bg-gray-50 dark:bg-white/5 divide-y divide-gray-200 dark:divide-gray-700">
        {children.map((op) => (
          <tr key={op.type}>
            <td className="px-4 py-3 whitespace-nowrap">
              <code className="text-sm bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 px-2 py-1 rounded">{op.type}</code>
            </td>
            <td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{op.desc}</td>
            <td className="px-4 py-3">
              <code className="text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 px-2 py-1 rounded block overflow-x-auto">{op.example}</code>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  </div>
);

# Operations

> See [Architecture](/architecture.md) for how operations are applied to the scene graph.

All operations follow the pattern: `dtype.verb`.
Operations marked as LWW use Last-Write-Wins semantics based on Lamport time.
Additive operations accumulate regardless of order. This can become a problem
when you divide a single atomic operation into two -- for instance for `text.replace`,
dividing it into `text.insert` and `text.delete` will result into the buffer
removing the first, since delete takes precedence when the two share the same index.

## Schema-less Design

Vuer-RTC is **schema-less**—type information is embedded in the operation itself via the `ot` field.
No separate schema definition is required. The `ot` string (e.g., `'vector3.add'`) is parsed to extract:

- **dtype**: The data type (`vector3`)
- **operation**: The merge behavior (`add`)

This enables dynamic properties without upfront schema definition.

## Operation Format

All operations are **flat** (no nested objects) except for node operations that send node data. Common fields:

```typescript
interface BaseOp {
  ot: string;        // Operation type: "dtype.verb"
  key?: string;         // Target node key ("." = root, default)
  path?: string;        // Property path on the node ("." = node root)
  value?: unknown;      // Value to apply (type depends on ot)
  // Additional flat fields per operation type:
  // index, from, to, alpha, separator, toPath, etc.
}
```

**Examples:**
```typescript
// Set position
{ ot: 'vector3.set', key: 'player-1', path: 'position', value: [1, 2, 3] }

// Blend color with alpha
{ ot: 'color.blend', key: 'sky', path: 'color', value: '#00ff00', alpha: 0.5 }

// Move array item
{ ot: 'array.move', key: 'item', path: 'tags', from: 0, to: 2 }

// Move node to new parent
{ ot: 'node.move', key: 'scene', path: 'children', value: { nodeKey: 'cube-1', newParent: 'group-1' } }

// Node insert (nested value for node data)
{ ot: 'node.insert', key: '.', path: 'children', value: { key: 'cube-1', tag: 'Mesh' } }
```

## Text CRDT Operations

Text operations use a compressed schema for efficient wire transfer. All text operations support both **position-based format** (local edits) and **CRDT format** (network sync with conflict resolution metadata).

### Compressed Schema

The compressed schema uses short field names and tuple encoding to minimize payload size:

**Field names:**
- `ot` — Operation type (full name, e.g., `'insert'`, `'text.insert'`)
- `id` — Unique item ID for inserts (e.g., `'alice:5'`)
- `value` — For inserts/replaces: `[anchor, content]` tuple
- `rm` — For deletes/replaces: array of `[itemId, length]` tuples
- `seq` — Lamport sequence number
- `ts` — Timestamp in seconds

### Rope CRDT Operations

Standalone TextRope operations (no `key`/`path` — used internally):

```typescript
// Insert
{
  ot: 'insert',
  id: 'alice:5',
  value: ['alice:4', 'hello'],  // [anchor, content]
  seq: 100,
  ts: 1234567890.123
}

// Delete
{
  ot: 'delete',
  rm: [['alice:5', 3], ['alice:10', 2]]  // [[itemId, length], ...]
}

// Replace (atomic delete + insert)
{
  ot: 'replace',
  rm: [['alice:5', 3], ['bob:7', 1]],    // deletions
  id: 'alice:8',
  value: ['alice:7', 'world'],            // [anchor, content]
  seq: 101,
  ts: 1234567890.456
}
```

### Graph Text Operations

Text operations on graph node properties (have `key`/`path`):

```typescript
// text.insert - Insert text with CRDT metadata
{
  ot: 'text.insert',
  key: 'node-1',
  path: 'description',
  id: 'alice:5',
  value: ['alice:4', 'hello'],  // [anchor, content]
  seq: 100,
  ts: 1234567890.123
}

// text.delete - Delete text spans
{
  ot: 'text.delete',
  key: 'node-1',
  path: 'description',
  rm: [['alice:5', 3], ['alice:10', 2]]
}

// text.replace - Atomic delete + insert
{
  ot: 'text.replace',
  key: 'node-1',
  path: 'description',
  rm: [['alice:5', 3]],
  id: 'alice:8',
  value: ['alice:7', 'world'],  // [anchor, content]
  seq: 101,
  ts: 1234567890.456
}

// text.init - Initialize CRDT text property
{
  ot: 'text.init',
  key: 'node-1',
  path: 'description',
  value: 'initial text'
}
```

### Position-Based Format

For local edits, you can use position-based format. The operation handlers automatically convert to CRDT format. Note that `value` is still the `[anchor, content]` tuple — pass `null` for the anchor and the CRDT computes it. **Prefer the higher-level helpers** (`insert()` / `doc.insert(pos, text)`), which build the tuple for you; hand-constructing an op with a bare-string `value` inserts `undefined`.

```typescript
// Insert at position (auto-converted to CRDT)
{
  ot: 'text.insert',
  key: 'node-1',
  path: 'description',
  position: 5,
  value: [null, 'hello']  // [anchor, content] — anchor null for position-based
}
// → Converted to CRDT format with id, anchor, seq, ts

// Delete at position
{
  ot: 'text.delete',
  key: 'node-1',
  path: 'description',
  position: 5,
  length: 3
}
// → Converted to CRDT format with rm spans

// Replace at position
{
  ot: 'text.replace',
  key: 'node-1',
  path: 'description',
  position: 5,
  length: 3,
  value: [null, 'new text']  // [anchor, content] — anchor null for position-based
}
// → Converted to CRDT format with rm spans and insert metadata
```

### Item IDs

Item IDs use the format `agentId:seq` where:
- `agentId` — Session/agent identifier (e.g., `'alice'`, `'session-abc123'`)
- `seq` — Local sequence number (increments per character inserted)

Example: `'alice:42'` means the 42nd character inserted by agent `alice`.

### YATA Ordering

The CRDT uses YATA (Yet Another Transformation Approach) for conflict resolution:
- Each character has a unique ID and parent reference (`anchor`)
- Insertions are ordered by: sequence number → timestamp → ID
- Concurrent inserts at the same position resolve deterministically

## Number

{[
  { type: 'number.set', desc: 'Set numeric value (LWW)', example: '{ ot: "number.set", key: "light-1", path: "intensity", value: 0.5 }' },
  { type: 'number.add', desc: 'Add to numeric value (additive)', example: '{ ot: "number.add", key: "player", path: "score", value: 10 }' },
  { type: 'number.multiply', desc: 'Multiply numeric value', example: '{ ot: "number.multiply", key: "sprite", path: "scale", value: 2 }' },
  { type: 'number.min', desc: 'Set to min(current, value)', example: '{ ot: "number.min", key: "enemy", path: "health", value: 0 }' },
  { type: 'number.max', desc: 'Set to max(current, value)', example: '{ ot: "number.max", key: "enemy", path: "health", value: 100 }' },
]}

## Vector3

{[
  { type: 'vector3.set', desc: 'Set position/scale (LWW)', example: '{ ot: "vector3.set", key: "cube", path: "position", value: [1, 2, 3] }' },
  { type: 'vector3.add', desc: 'Add to vector (additive)', example: '{ ot: "vector3.add", key: "player", path: "position", value: [0, 1, 0] }' },
  { type: 'vector3.multiply', desc: 'Component-wise multiply', example: '{ ot: "vector3.multiply", key: "mesh", path: "scale", value: [2, 2, 2] }' },
  { type: 'vector3.applyEuler', desc: 'Rotate by euler angles (radians)', example: '{ ot: "vector3.applyEuler", key: "arrow", path: "direction", value: [0, 1.57, 0], order: "YXZ" }' },
  { type: 'vector3.applyQuaternion', desc: 'Rotate by quaternion', example: '{ ot: "vector3.applyQuaternion", key: "arrow", path: "direction", value: [0, 0.7, 0, 0.7] }' },
]}

## Euler

{[
  { type: 'euler.set', desc: 'Set euler angles (LWW)', example: '{ ot: "euler.set", key: "camera", path: "rotation", value: [0, 1.57, 0] }' },
  { type: 'euler.add', desc: 'Add to euler angles (additive)', example: '{ ot: "euler.add", key: "turret", path: "rotation", value: [0.1, 0, 0] }' },
]}

## Quaternion

{[
  { type: 'quaternion.set', desc: 'Set rotation (LWW)', example: '{ ot: "quaternion.set", key: "bone", path: "rotation", value: [0, 0, 0, 1] }' },
  { type: 'quaternion.multiply', desc: 'Compose rotations', example: '{ ot: "quaternion.multiply", key: "joint", path: "rotation", value: [0, 0.7, 0, 0.7] }' },
]}

## Color

{[
  { type: 'color.set', desc: 'Set hex color (LWW)', example: '{ ot: "color.set", key: "material-1", path: "color", value: "#ff0000" }' },
  { type: 'color.blend', desc: 'Blend towards color', example: '{ ot: "color.blend", key: "sky", path: "color", value: "#00ff00", alpha: 0.5 }' },
]}

## String

For `move` operations, `to` refers to the **current index** (before the move), not the post-move index. Negative indices count from the end.

```
"Hello World" → move [0:5] to index 6  → " WorldHello"
"Hello World" → move [0:5] to index -1 → " WorldHello"
```

{[
  { type: 'string.set', desc: 'Set string value (LWW)', example: '{ ot: "string.set", key: "label", path: "text", value: "Player 1" }' },
  { type: 'string.concat', desc: 'Append to string', example: '{ ot: "string.concat", key: "console", path: "log", value: "event", separator: "\\n" }' },
  { type: 'string.insert', desc: 'Insert at index', example: '{ ot: "string.insert", key: "label", path: "text", value: "Hello ", index: 0 }' },
  { type: 'string.cut', desc: 'Remove substring', example: '{ ot: "string.cut", key: "label", path: "text", from: 0, to: 5 }' },
  { type: 'string.replace', desc: 'Replace substring', example: '{ ot: "string.replace", key: "label", path: "text", from: 0, to: 5, value: "Hi" }' },
  { type: 'string.move', desc: 'Move substring (to = current index)', example: '{ ot: "string.move", key: "label", path: "text", from: 0, to: 5, index: 10 }' },
]}

## Boolean

{[
  { type: 'boolean.set', desc: 'Set boolean (LWW)', example: '{ ot: "boolean.set", key: "mesh", path: "visible", value: true }' },
  { type: 'boolean.or', desc: 'OR operation', example: '{ ot: "boolean.or", key: "state", path: "dirty", value: true }' },
  { type: 'boolean.and', desc: 'AND operation', example: '{ ot: "boolean.and", key: "button", path: "enabled", value: false }' },
  { type: 'boolean.xor', desc: 'XOR operation (toggle)', example: '{ ot: "boolean.xor", key: "light", path: "on", value: true }' },
]}

## Array

For `move` operations, `to` refers to the **current index** (before the move), not the post-move index. Negative indices count from the end.

```
["a", "b", "c", "d"] → move index 0 to index 2  → ["b", "c", "a", "d"]
["a", "b", "c", "d"] → move index 0 to index -2 → ["b", "c", "a", "d"]
```

{[
  { type: 'array.set', desc: 'Replace array (LWW)', example: '{ ot: "array.set", key: "item", path: "tags", value: ["a", "b"] }' },
  { type: 'array.push', desc: 'Append item', example: '{ ot: "array.push", key: "item", path: "tags", value: "new-tag" }' },
  { type: 'array.insert', desc: 'Insert at index', example: '{ ot: "array.insert", key: "item", path: "tags", value: "tag", index: 1 }' },
  { type: 'array.remove', desc: 'Remove by value or index', example: '{ ot: "array.remove", key: "item", path: "tags", value: "old-tag", index: 2 }' },
  { type: 'array.move', desc: 'Move item (to = current index)', example: '{ ot: "array.move", key: "item", path: "tags", from: 0, to: 2 }' },
  { type: 'array.union', desc: 'Add unique items', example: '{ ot: "array.union", key: "item", path: "tags", value: ["x", "y"] }' },
]}

## Object

{[
  { type: 'object.set', desc: 'Replace object (LWW)', example: '{ ot: "object.set", key: "entity", path: "metadata", value: { a: 1 } }' },
  { type: 'object.update', desc: 'Shallow merge', example: '{ ot: "object.update", key: "entity", path: ".", value: { visible: true } }' },
  { type: 'object.remove', desc: 'Remove key from object', example: '{ ot: "object.remove", key: "entity", path: "metadata.tempKey" }' },
]}

## Node

Structural operations for the scene graph.

{[
  { type: 'node.insert', desc: 'Insert child node under parent', example: '{ ot: "node.insert", key: ".", path: "children", value: { key: "cube-1", tag: "Mesh" } }' },
  { type: 'node.remove', desc: 'Delete node (tombstone)', example: '{ ot: "node.remove", key: "cube-1", path: "." }' },
  { type: 'node.upsert', desc: 'Insert or merge if exists', example: '{ ot: "node.upsert", key: "scene", path: "children", value: { key: "cube-1", position: [1,2,3] } }' },
  { type: 'node.inset', desc: 'Insert or set if exists', example: '{ ot: "node.inset", key: "scene", path: "children", value: { key: "cube-1", tag: "Mesh", visible: true } }' },
  { type: 'node.move', desc: 'Move node to new parent', example: '{ ot: "node.move", key: "scene", path: "children", value: { nodeKey: "cube-1", newParent: "group-1" } }' },
]}

## Message Format

Operations are wrapped in a `CRDTMessage` that includes metadata for conflict resolution:

```typescript
interface CRDTMessage {
  id: string;        // Unique message ID
  client: string;    // Client session ID
  clock: VectorClock;// Vector clock for causality
  lt: number;        // Lamport time for total ordering
  ts: number;        // Wall-clock timestamp
  ops: Operation[];  // Batch of operations
}
```

## Euler Rotation Order

The `vector3.applyEuler` operation supports two optional parameters:

- **`order`**: Rotation order - `'XYZ'` (default), `'YXZ'`, `'ZXY'`, `'ZYX'`, `'YZX'`, `'XZY'`
- **`intrinsic`**: `true` (default) for intrinsic rotation, `false` for extrinsic

### Intrinsic vs. Extrinsic Rotations

- **Intrinsic** (default): Axes move with the body. Rotate around body's X axis, then around the *new* Y axis, then around the *new* Z axis.
- **Extrinsic**: Axes stay fixed in space. Rotate around fixed X, then fixed Y, then fixed Z.

**Note**: Intrinsic XYZ is equivalent to Extrinsic ZYX (reversed order).

```typescript
// Intrinsic YXZ rotation (common for cameras/FPS)
{ ot: 'vector3.applyEuler', key: 'camera', path: 'direction', value: [pitch, yaw, roll], order: 'YXZ' }

// Extrinsic XYZ rotation (fixed-axis)
{ ot: 'vector3.applyEuler', key: 'arm', path: 'direction', value: [x, y, z], intrinsic: false }
```

### Common Use Cases

| Order | Use Case |
|-------|----------|
| `XYZ` | Default, general 3D rotations |
| `YXZ` | First-person cameras (yaw-pitch-roll) |
| `ZYX` | Aircraft/aerospace conventions |
| `ZXY` | Some motion capture systems |

---

**Try it live:** See operations in action in the [Live Demo](/demo).
