<!-- Generated from reference/types.mdx; edit the MDX source. -->

Package: @liveroom-tech/react-immersive@5.2.0
Canonical: https://react-immersive.liveroom.dev/docs/reference/types

# Types Reference

## `ObjectBinding`

The core data record for each interactive mesh in the viewer.

```ts
type ObjectBinding = {
  id: string;
  type?: ObjectBindingType; // defaults to "other"
  modelObjectId: string;
  label?: string;
  group?: string;
  tags?: string[];
  status?: "normal" | "hidden" | "selected" | "disabled";
  visible?: boolean;
  selectable?: boolean;
  hoverable?: boolean;
  transform?: ObjectBindingTransform;
  style?: ObjectBindingStyle;
  decals?: ObjectBindingDecal[]; // images and text on the surface; see ModelViewer's "Decals"
  actions?: ObjectBindingAction[];
  metrics?: Record<string, number>;
  metadata?: Record<string, unknown>;
  cameraState?: ObjectBindingCameraState;
};
```

### Key Notes

- The `objectBindings` record is **keyed by model node identifier**, not by `id`; unique names remain unchanged and duplicates use suffixed IDs such as `Cube [1]`
- `group` assigns a binding to one named set, while `tags` can assign it to several queryable sets
- `status` stores the authoring/domain status assigned in `BindingBuilder`
- `transform` stores persistent local-space position, rotation, and scale overrides; rotation values use radians
- `visible` is live render state, not just an initial default
- `selectable: false` and `hoverable: false` make the mesh inert to viewport picking; clicks resolve the front-most surface under the pointer, so a non-selectable mesh does **not** pass the click through to meshes behind it
- `style.material.baseColor` is the committed source of truth for color picks
- `style.material.texture.path` is the committed source of truth for texture uploads
- If no `texture.path` and no `style.material.baseColor` are set, the viewer falls back to the original model material
- See [`ObjectBindingMaterial`](https://react-immersive.liveroom.dev/docs/reference/types#objectbindingmaterial) for the full set of per-object material overrides
- `metrics` and `metadata` are displayed in the side panel as nested key/value fields (objects expand into indented child labels)
- `cameraState` stores a saved camera view for the object; when both `position` and `target` are set, the viewer uses that view when the object is focused or selected instead of falling back to `fitToBox`

## `ObjectBindingStatus`

```ts
type ObjectBindingStatus = "normal" | "hidden" | "selected" | "disabled";
```

This is the value stored in `ObjectBinding.status` and edited by
`BindingBuilder`'s Basics panel. Use `visible`, `selectable`, and `hoverable`
for the corresponding live viewer behavior.

## `defineObjectBindings`

A runtime identity helper that validates binding literals while preserving their exact inferred TypeScript types.

```ts
import {
  defineObjectBindings,
  type ObjectBindingActionId,
  type ObjectBindingKey,
} from "@liveroom-tech/react-immersive/utils";

const bindings = defineObjectBindings({
  body: {
    id: "vehicle-body",
    modelObjectId: "body",
    type: "body",
    actions: [{ id: "repaint", label: "Repaint", type: "command" }],
  },
});

type BindingKeys = ObjectBindingKey<typeof bindings>; // "body"
type BodyActions = ObjectBindingActionId<typeof bindings, "body">; // "repaint"
```

`ObjectBindingKey<T>` extracts the binding-map key union. `ObjectBindingActionId<T, K>` extracts the declared action-id union for binding `K`.

## `ObjectBindingTransform`

```ts
type ObjectBindingTransform = {
  position?: [number, number, number]; // local model units
  rotation?: [number, number, number]; // local Euler angles in radians
  scale?: [number, number, number];
};
```

Missing fields retain their model-authored local values. Removing the complete
transform restores the model-authored position, rotation, and scale.

`ModelViewer` and `BindingBuilder` convert World/Local gizmo interactions back
into this local-space data before saving. BindingBuilder displays rotation in
degrees for authoring, but exports radians. Its transform gizmo is positioned
at the center of the object's renderable geometry so an object can rotate around
itself even when its source node origin is offset; that temporary authoring
pivot does not modify the model file's origin.

---

## `ObjectBindingType`

What kind of object a binding is, used to group and filter bindings (for
example `hideObject({ type: "wheel" })`). Any string works. The listed values
cover vehicles, rooms, furniture, characters, weapons, environment, and more,
and your editor suggests them; a domain they don't cover can use its own, such
as `"bone"` or `"turbine-blade"`. A binding without a `type` counts as
`"other"`.

```ts
type ObjectBindingType =
  | "body"
  | "light"
  | "wheel"
  | "glass"
  | "interior"
  | "floor"
  | "wall"
  | "door"
  | "furniture"
  | "decor"
  | "electronics"
  | "other"
  // ... and more suggestions
  | (string & {}); // any other string
```

---

## `ObjectBindingStyle`

Controls the visual appearance of a mesh. All visual overrides live under
`material` (an `ObjectBindingMaterial`); there are no style-level `color` /
`metalness` / `roughness` fields.

```ts
type ObjectBindingStyle = {
  material?: ObjectBindingMaterial;
};
```

---

## `ObjectBindingMaterial`

Per-object overrides applied on top of the mesh's original model material as a
`MeshPhysicalMaterial`. Every field is optional, only the fields you set are
overridden; everything else falls back to the model-authored value. Texture slots
are `{ path: string }` objects.

```ts
type TextureSlot = { path: string };

type ObjectBindingMaterial = {
  // ── Base / PBR ──────────────────────────────────────────────
  texture?: TextureSlot; // base color (albedo) map
  baseColor?: string; // hex, e.g. "#ff0000"
  metalness?: number; // 0–1
  metalnessMap?: TextureSlot;
  roughness?: number; // 0–1
  roughnessMap?: TextureSlot;
  reflectivity?: number; // 0–1 (specular F0)
  specularIntensity?: number; // 0–1
  specularIntensityMap?: TextureSlot;
  specularColor?: string; // hex
  specularColorMap?: TextureSlot;

  // ── Emission ────────────────────────────────────────────────
  emissive?: string; // hex
  emissiveIntensity?: number; // 0–10
  emissiveMap?: TextureSlot;

  // ── Opacity / blending ──────────────────────────────────────
  opacity?: number; // 0–1
  alphaMap?: TextureSlot;
  alphaTest?: number; // 0–1
  alphaHash?: boolean;
  dithering?: boolean; // banding reduction on gradients
  blendingMode?: "normal" | "additive";

  // ── Normal / bump ───────────────────────────────────────────
  normalMap?: TextureSlot;
  normalScale?: number;
  flipNormalY?: boolean;
  bumpMap?: TextureSlot;
  bumpScale?: number;

  // ── Displacement ────────────────────────────────────────────
  displacementMap?: TextureSlot;
  displacementScale?: number;
  displacementBias?: number;

  // ── Ambient occlusion ───────────────────────────────────────
  aoMap?: TextureSlot;
  aoMapIntensity?: number;

  // ── Clearcoat ───────────────────────────────────────────────
  clearcoat?: number; // 0–1
  clearcoatMap?: TextureSlot;
  clearcoatRoughness?: number; // 0–1
  clearcoatRoughnessMap?: TextureSlot;
  clearcoatNormalMap?: TextureSlot;
  clearcoatNormalScale?: number;
  flipClearcoatNormalY?: boolean;

  // ── Sheen ───────────────────────────────────────────────────
  sheen?: number; // 0–1
  sheenColor?: string; // hex
  sheenColorMap?: TextureSlot;
  sheenRoughness?: number; // 0–1
  sheenRoughnessMap?: TextureSlot;

  // ── Anisotropy ──────────────────────────────────────────────
  anisotropy?: number; // 0–1
  anisotropyRotation?: number; // radians
  anisotropyMap?: TextureSlot;

  // ── Transmission / volume ───────────────────────────────────
  transmission?: number; // 0–1
  transmissionMap?: TextureSlot;
  thickness?: number;
  thicknessMap?: TextureSlot;
  attenuationColor?: string; // hex (volume tint)
  attenuationDistance?: number;

  // ── Faces ───────────────────────────────────────────────────
  side?: "front" | "back" | "double";
};
```

### Texture color space

Texture slots are decoded with the correct color space automatically, so maps
render at the right brightness:

- **sRGB (color) maps:** `texture` (base color/albedo), `emissiveMap`,
  `sheenColorMap`, `specularColorMap`
- **Linear (data) maps:** everything else, `normalMap`, `roughnessMap`,
  `metalnessMap`, `aoMap`, `displacementMap`, `bumpMap`, `alphaMap`,
  `clearcoat*Map`, `anisotropyMap`, `transmissionMap`, `thicknessMap`,
  `sheenRoughnessMap`, `specularIntensityMap`

The shared texture cache is keyed by URL **and** color space, so the same image
URL can be used safely as both a color map and a data map.

The package exports `MATERIAL_SIDES` (`["front", "back", "double"]`) and
`MATERIAL_BLENDING_MODES` (`["normal", "additive"]`) for building typed UI
controls without duplicating these option lists.

---

## `ObjectBindingAction`

Defines an action button shown in the side panel.

```ts
type ObjectBindingAction = {
  id: string;
  label: string;
  type: "command";
  effects?: ObjectActionEffect[];
};
```

`effects` are executed in declaration order by `useViewerActions`. Each effect updates one concern through the corresponding library controller:

```ts
type ObjectActionEffect =
  | {
      target: ObjectBindingTarget;
      objectBindings: ObjectBindingPatch;
    }
  | { sceneConfig: SceneConfigPatch }
  | {
      target: ObjectBindingTarget;
      visibility: "show" | "hide" | "toggle";
    }
  | { camera: CameraAction }
  | { animation: AnimationAction };

type CameraAction =
  | { type: "focus-object"; objectId?: string }
  | { type: "fit-scene" }
  | { type: "reset" }
  | { type: "set-target"; target: [number, number, number]; transition?: boolean }
  | { type: "set-state"; state: ViewerCameraState; transition?: boolean };

// play replaces the clips already playing unless alongside is set; the others
// act on clip, or on every active clip when it's left out.
type AnimationAction =
  | ({ type: "play"; clip: string } & AnimationPlayOptions)
  | { type: "pause"; clip?: string }
  | { type: "stop"; clip?: string }
  | { type: "set-speed"; speed: number; clip?: string }
  | { type: "seek"; time: number; clip?: string };
```

`ObjectBindingTarget` supports an object id, an array of ids, `{ group }`, `{ tag }`, or `{ type }`.

Built-in action IDs:

| ID                  | Behavior               |
| ------------------- | ---------------------- |
| `toggle-visibility` | Toggle mesh visibility |
| `change-color`      | Open color picker      |
| `change-material`   | Open texture upload    |

---

## `ObjectBindingCameraState`

Saved camera state for zooming to an object.

```ts
type ObjectBindingCameraState = {
  position?: [number, number, number];
  target?: [number, number, number];
  fov?: number;
  zoom?: number;
};
```

When both `position` and `target` are set on a binding's `cameraState`, the viewer uses that saved view when the object is focused or selected instead of falling back to `fitToBox`.

---

## `ObjectActionEvent`

The event shape emitted by `onAction` and `useViewerActions.runAction`.

```ts
type ObjectActionEvent = {
  objectId: string;
  action: ObjectBindingAction;
  binding?: ObjectBinding;
  screenX?: number;
  screenY?: number;
};
```

---

## `ObjectPointerEvent`

The event shape passed to `onObjectDoubleClick` and `onObjectContextMenu`.

```ts
type ObjectPointerEvent = {
  binding: ObjectBinding;
  clientX: number; // viewport coordinates, e.g. for a position: fixed menu
  clientY: number;
  pointerType: string; // "mouse" | "pen" | "touch"
  surface?: { position: Vec3; normal: Vec3 }; // where on the object, in its own space
};
```

---

## `AnimationControls`

Provided via `onAnimationsReady`.

```ts
type AnimationPlayOptions = {
  alongside?: boolean; // keep the clips already playing (default false: replace them)
  fade?: number; // seconds to fade in, and to fade out the clips it replaces
  reverse?: boolean; // play backwards, from where the clip is or from its end
};

type AnimationClipState = {
  clip: string;
  status: "playing" | "paused" | "finished"; // finished: a play-once clip holding its last pose
  time: number; // seconds
  duration: number; // seconds
  speed: number;
  reverse: boolean;
};

type AnimationPlaybackState = {
  currentClip: string | null; // the clip played most recently, while it's active
  isPlaying: boolean; // whether any clip is playing
  speed: number; // speed, position and length of currentClip
  time: number;
  duration: number;
  active: AnimationClipState[]; // every active clip, most recently played last
};

// Leave out clipName to act on every active clip.
type AnimationControls = {
  clips: string[];
  clipDetails?: { sourceName: string; duration: number }[];
  play: (clipName: string, options?: AnimationPlayOptions) => void;
  pause: (clipName?: string) => void;
  stop: (clipName?: string) => void; // stops and resets the pose
  setSpeed: (speed: number, clipName?: string) => void;
  seek?: (time: number, clipName?: string) => void; // absolute time, in seconds
  getState?: () => AnimationPlaybackState;
  subscribe?: (listener: (state: AnimationPlaybackState) => void) => () => void;
};
```

---

## `ViewerIsolation`

`ModelViewer`'s `isolation` prop. See
[Isolating objects](https://react-immersive.liveroom.dev/docs/reference/model-viewer#isolating-objects).

```ts
type ViewerIsolation = {
  target: ObjectBindingTarget; // an id, a list of ids, { group }, { tag }, or { type }
  mode?: "ghost" | "hide"; // default "ghost"
  ghostOpacity?: number; // 0–1, default 0.12
};
```

---

## `SceneSectionConfig`

`sceneConfig.section`. See
[`showSectionTools`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#showsectiontools).

```ts
type SceneSectionConfig = {
  enabled: boolean;
  axis: "x" | "y" | "z"; // world axis the cut runs across
  position: number; // 0 at the model's minimum along the axis, 1 at its maximum
  flip: boolean; // keep the higher side instead of the lower one
};
```

`DEFAULT_SECTION_CONFIG` is `{ enabled: false, axis: "y", position: 0.5, flip: false }`.

---

## `SceneStyleRule`

The rules in `sceneConfig.styleRules`, applied in order. See
[Style rules](https://react-immersive.liveroom.dev/docs/reference/model-viewer#style-rules).

```ts
type SceneStyleRule = {
  id: string;
  label?: string;
  target?: ObjectBindingTarget; // every bound object when left out
  when?: SceneStyleCondition | SceneStyleCondition[]; // all must match
  material?: ObjectBindingMaterial;
  colorScale?: SceneStyleColorScale;
  pulse?: boolean | SceneStylePulse;
};

type SceneStyleCondition =
  | { metric: string; above?: number; below?: number }
  | { status: ObjectBindingStatus | ObjectBindingStatus[] }
  | { metadata: string; equals: string | number | boolean | null }; // "a.b" reaches nested keys

type SceneStyleColorScale = {
  metric: string;
  stops: [number, string][]; // [value, "#rrggbb"]
  channel?: "baseColor" | "emissive"; // default "baseColor"
};

type SceneStylePulse = {
  color?: string; // default "#ff3b30"
  speed?: number; // pulses per second, default 1
};
```

---

## `SceneLight`

```ts
type SceneLightAttachment = {
  objectId: string;
  offset: [number, number, number];
};

type SceneLight = {
  id: string;
  type: "none" | "directional" | "point" | "spot" | "hemisphere";
  color: string;
  intensity: number;
  position: [number, number, number];
  rotation?: [number, number, number];
  distance?: number;
  decay?: number;
  angle?: number;
  penumbra?: number;
  attachedToCamera: boolean;
  attachment?: SceneLightAttachment;
  castShadow: boolean;
  shadowBias: number;
  visible: boolean;
};
```

`position` and `rotation` are world-space unless the light is attached.
`rotation` uses Euler radians and controls directional/spot-light orientation.
An attachment resolves
binding keys, binding ids, `modelObjectId` values, and raw model node names. Its
offset uses the target object's local space. `distance` and `decay` apply to
point and spot lights; spot lights also use `angle` (radians) and `penumbra`
(`0`–`1`).

---

## `SceneModelRenderer`

Selects the whole-model material renderer used by `SceneConfig.model.renderer`.

```ts
type SceneModelRenderer = "pbr" | "matcap" | "uv-checker";
```

- `"pbr"` renders the model-authored and binding-overridden physical materials.
- `"matcap"` renders a lighting-independent studio material.
- `"uv-checker"` renders a numbered directional UV0 test chart. Meshes without
  UV0 coordinates appear magenta.

---

## `SceneConfig`

The complete serializable scene state consumed by `ModelViewer.sceneConfig`,
authored by `BindingBuilder`, and managed by `useSceneConfig`.

```ts
type SceneConfig = {
  model: {
    rotation: [number, number, number];
    showAdvancedRotation: boolean;
    renderer: "pbr" | "matcap" | "uv-checker";
    shading: "lit" | "unlit" | "normals";
    autoRotate?: boolean;
    autoRotateSpeed?: number;
    activeVariants?: string[]; // glTF material variants shown, one per option group
  };
  camera: SceneCameraConfig;
  lighting: SceneLightingConfig;
  wireframe: SceneWireframeConfig;
  shadows: SceneShadowConfig;
  environment: SceneEnvironmentConfig;
  background: SceneBackgroundConfig;
  groundShadows: SceneGroundShadowConfig;
  postProcessing: ScenePostProcessingConfig;
  animations: SceneAnimationConfig;
  annotations: SceneAnnotationMarker[];
  cinematic?: SceneCinematicConfig;
  styleRules?: SceneStyleRule[]; // data-driven looks; see ModelViewer's "Style rules"
  section?: SceneSectionConfig; // cut-away view; see ModelViewer's showSectionTools
};
```

`model.autoRotate` and `model.autoRotateSpeed` apply when the equivalent
`ModelViewer` props aren't passed; passed props win. `model.activeVariants`
selects the model's glTF material variants by name, at most one from each
option group (see
[`showMaterialVariants`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#showmaterialvariants)). Import
`DEFAULT_SCENE_CONFIG` when creating a scene from code so new sections and
defaults remain populated, then update it with `useSceneConfig` or
`patchSceneConfig`. For `lighting.lights`, `sceneLight({ id, ...settings })`
builds a complete light, filling in the settings you leave out.

---

## 3D Tiles types

### `ViewerObjectInfo`

`onObjectCatalog` receives one entry for every bindable object in the model,
whether it's loaded whole or streamed:

```ts
type ViewerObjectInfo = {
  name: string; // the key to use in objectBindings
  triangles: number;
  minimum: [number, number, number];
  maximum: [number, number, number];
};
```

`minimum` and `maximum` are the object's bounds in the model's own units, in
Z-up coordinates as in 3D Tiles: a point at glTF `(x, y, z)` is `(x, -z, y)`
here. They don't change when the viewer moves or scales the model.

---

## `CinematicConfig`

Configures the [`cinematic`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#cinematic) auto-camera on `ModelViewer`.

```ts
type CinematicConfig = {
  waypoints?: CinematicWaypoint[]; // camera keyframes to glide through
  duration?: number; // seconds for one full pass (auto-scales when omitted)
  loop?: boolean; // default true
  autoPlay?: boolean; // start once the model has loaded and framed (default false)
};

type CinematicWaypoint = {
  position: [number, number, number];
  target: [number, number, number]; // world-space look-at point
  azimuthAngle?: number; // radians, captured by BindingBuilder; omit by hand
  polarAngle?: number; // radians
  segmentDuration?: number; // seconds spent reaching the next waypoint
};
```

With fewer than two waypoints the camera falls back to an auto-generated orbit around the model's bounding sphere.

---

## `SceneCinematicConfig`

The persisted form of the cinematic path carried on `SceneConfig.cinematic`, authored visually in `BindingBuilder`'s Cinematic tab.

```ts
type SceneCinematicConfig = {
  enabled: boolean; // turns the auto-camera on at runtime
  waypoints: SceneCinematicWaypoint[];
  duration?: number;
  loop: boolean;
  autoPlay: boolean;
};

type SceneCinematicWaypoint = {
  position: [number, number, number];
  target: [number, number, number];
  azimuthAngle?: number; // radians, captured by BindingBuilder; omit by hand
  polarAngle?: number; // radians
  segmentDuration?: number;
};
```

`ModelViewer` activates a `sceneConfig`-authored path when `enabled` is `true` and there are at least two waypoints. The [`cinematic`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#cinematic) prop overrides it field-by-field.

---

## `CustomObjectBindingDataPanelProps`

Props passed to a `customObjectBindingDataPanel` render prop.

```ts
type CustomObjectBindingDataPanelProps = {
  isOpen: boolean;
  selectedObject: ObjectBinding | null;
  currentAction: ObjectActionEvent | null;
  onClose: () => void;
  onAction: (event: ObjectActionEvent) => void;
};
```

---

## `CustomSceneObjectsPanelProps`

Props passed to a `customSceneObjectsPanel` render prop.

```ts
type CustomSceneObjectsPanelProps = {
  objectBindings: Record<string, ObjectBinding>;
  onAction?: (event: ObjectActionEvent) => void;
  onFocus?: (binding: ObjectBinding) => void;
  onHover?: (binding: ObjectBinding | null) => void;
};
```

## `CaptureVideoOptions`

Options for `ViewerReadyState.captureVideo`, `useViewerConnection().captureVideo`,
and `useViewer().connection.captureVideo`. The result is a video `Blob` in the
browser-supported format. See [Video export](https://react-immersive.liveroom.dev/docs/reference/model-viewer#video-export)
for readiness requirements and recording behavior.

```ts
type CaptureVideoOptions = {
  /** Defaults to cinematic when a path is enabled, otherwise turntable. */
  mode?: "cinematic" | "turntable";
  /** Auto prefers WebM, then MP4. "gif" makes a looping GIF. Explicit formats reject if unsupported. */
  format?: "auto" | "webm" | "mp4" | "gif";
  /** Seconds per pass, in (0, 300]. Defaults to path timing, or a 24s orbit (8s for a GIF). */
  duration?: number;
  /** Capture frame rate, from 1 to 60. Default 30 (15 for a GIF). */
  fps?: number;
  /** Encoder target bitrate in bits per second. Default 8,000,000. Not used for GIFs. */
  videoBitsPerSecond?: number;
  /** GIF width in pixels, 16 to 4096, never wider than the canvas; the height follows. Default 480. */
  gifWidth?: number;
  /** Aborting discards the recording and restores the viewer. */
  signal?: AbortSignal;
};
```

## `ViewerEngagementEvent`

The discriminated event union received by `ModelViewer.onEngagement`. Narrow on
`event.type` to access event-specific fields. See
[Engagement analytics](https://react-immersive.liveroom.dev/docs/reference/model-viewer#onengagement--debugengagement)
for event triggers, opt-in logging, and session semantics.

```ts
/** Engagement from deliberate viewer interactions, with model/session context. */
type ViewerEngagementEvent = {
  /** Ephemeral identifier for this viewer/model session; no persistent storage. */
  sessionId: string;
  /** ISO timestamp of the event. */
  timestamp: string;
  /** Milliseconds since tracking started for this model. */
  elapsedMs: number;
  modelUrl: string;
  tilesetUrl: string | null;
} & (
  | {
      type: "part-viewed";
      objectId: string;
      objectKey: string;
      label?: string;
      source: "canvas" | "panel" | "keyboard";
    }
  | {
      type: "variant-chosen";
      variant: string | null; // null when a group's choice was cleared
      previousVariant: string | null; // the group's previous choice
      group: string | null; // the option group's name; null for an unnamed group
    }
  | { type: "ar-started" }
  | {
      type: "ar-ended";
      durationMs: number;
      reason: "session-ended" | "interrupted";
    }
  | {
      type: "ar-launch-requested";
      channel: "quick-look" | "mobile-handoff";
    }
  | { type: "tour-started"; annotationCount: number }
  | { type: "tour-completed"; annotationCount: number; durationMs: number }
  | {
      type: "tour-stopped";
      annotationCount: number;
      durationMs: number;
      reason: "stopped" | "restarted" | "annotations-changed" | "interrupted";
    }
);

```

## SceneAnnotationMarker

```ts
type SceneAnnotationCameraView = {
  position: [number, number, number];
  target: [number, number, number];
  azimuthAngle?: number; // radians
  polarAngle?: number; // radians
  fov?: number | null;
  zoom?: number;
};

type SceneAnnotationImage = { src: string; alt?: string };
type SceneAnnotationLink = { href: string; label: string };

type SceneAnnotationMarker = {
  id: number;
  worldPosition: [number, number, number];
  localPosition: [number, number, number];
  worldNormal?: [number, number, number];
  anchorNodeName?: string;
  anchorLocalPosition?: [number, number, number];
  title: string;
  description: string;
  cameraView?: SceneAnnotationCameraView;
  images?: SceneAnnotationImage[];
  links?: SceneAnnotationLink[];
  category?: string;
  minDistance?: number;
  maxDistance?: number;
};
```

All rich fields are optional. See [Rich annotations](https://react-immersive.liveroom.dev/docs/reference/model-viewer#rich-annotations) for saved-view playback,
category filtering, and marker-distance behavior. These annotation types are
exported from the main entry and the server-safe `/utils` entry.

## Other exported types

Every type below is exported from `@liveroom-tech/react-immersive` and is
described with the API it belongs to.

| Types | Described with |
| --- | --- |
| `ModelViewerProps`, `ModelViewerPreset`, `GroupedModelViewerProps`, `ModelViewerUiOptions`, `ModelViewerToolsOptions`, `ModelViewerControlsOptions`, `ModelViewerPerfOptions`, `ModelViewerTilesOptions`, `ModelViewerXrOptions`, `ModelViewerDecoderOptions`, `ObjectTransformSpace`, `TexturePositioningControls`, `XRScaleMode`, `ModelFormat`, `AnnotationMarker`, `CameraLimits`, `CameraControllerType`, `MouseControllerPosition`, `PerformanceProfile`, `ViewerObjectInfo` | [`ModelViewer`](https://react-immersive.liveroom.dev/docs/reference/model-viewer) |
| `SimpleModelViewerProps` (from `/simple-model-viewer`) | [`SimpleModelViewer`](https://react-immersive.liveroom.dev/docs/reference/simple-model-viewer) |
| `BindingBuilderProps` (from `/binding-builder`) | [`BindingBuilder`](https://react-immersive.liveroom.dev/docs/reference/binding-builder) |
| `ObjectBindingMaterialBlendingMode`, `ObjectBindingMaterialSide`, `ObjectBindingTextureSlot`, `ObjectBindingTextureChannel` | [`ObjectBindingMaterial`](https://react-immersive.liveroom.dev/docs/reference/types#objectbindingmaterial) |
| `AnimationActionEffect`, `CameraActionEffect`, `ObjectBindingsActionEffect`, `SceneConfigActionEffect`, `VisibilityActionEffect` | [`ObjectBindingAction`](https://react-immersive.liveroom.dev/docs/reference/types#objectbindingaction) |
| `AnimationLoopMode`, `SceneAnimationClipConfig`, `SceneLightType`, `SceneModelConfig`, `SceneCameraConfig`, `SceneLightingConfig`, `SceneShadowConfig`, `SceneEnvironmentConfig`, `SceneWireframeConfig`, `SceneBackgroundConfig`, `SceneGroundShadowConfig`, `ScenePostProcessingConfig`, `EnvironmentPreset`, `GlitchModeName`, `KernelSizeName`, `RampTypeName` | [`SceneConfig`](https://react-immersive.liveroom.dev/docs/reference/types#sceneconfig) and [`SceneLight`](https://react-immersive.liveroom.dev/docs/reference/types#scenelight) |
| `SceneAnnotationMarker`, `SceneAnnotationCameraView`, `SceneAnnotationImage`, `SceneAnnotationLink` | [`SceneAnnotationMarker`](https://react-immersive.liveroom.dev/docs/reference/types#sceneannotationmarker) |
| `ObjectBindingDecal`, `ObjectSurfacePoint` | [Decals](https://react-immersive.liveroom.dev/docs/reference/model-viewer#decals) |
| `MaterialVariantGroup`, `MaterialVariantOption` (also from `/simple-model-viewer` and `/utils`) | [`showMaterialVariants`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#showmaterialvariants) |
| `ReducedMotionSetting` (also from `/simple-model-viewer` and `/utils`) | [`reducedMotion`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#reducedmotion) |
| `RendererBackend` (also from `/utils`) | [`webgpu`](https://react-immersive.liveroom.dev/docs/reference/model-viewer#webgpu) |
| `ViewerLoading`, `ViewerReveal` (also from `/simple-model-viewer`) | [Lazy loading and posters](https://react-immersive.liveroom.dev/docs/reference/model-viewer#lazy-loading-and-posters) |
| `CompareViewerProps`, `CompareViewerSide` | [`CompareViewer`](https://react-immersive.liveroom.dev/docs/reference/compare-viewer) |
| `ViewerIsolation`, `IsolatedObjectBindings` | [`ViewerIsolation`](https://react-immersive.liveroom.dev/docs/reference/types#viewerisolation) |
| `ViewerIsolationProps` | [`useViewerIsolation`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-isolation) |
| `SceneSectionConfig`, `SceneSectionAxis` | [`SceneSectionConfig`](https://react-immersive.liveroom.dev/docs/reference/types#scenesectionconfig) |
| `SceneStyleRule`, `SceneStyleCondition`, `SceneStyleColorScale`, `SceneStylePulse` | [`SceneStyleRule`](https://react-immersive.liveroom.dev/docs/reference/types#scenestylerule) |
| `StyledObjectBindings`, `StyledObjectPulse` | `applyStyleRules`, see [Style rules](https://react-immersive.liveroom.dev/docs/reference/model-viewer#style-rules) |
| `HiddenObjectsState`, `ObjectBindingsController`, `BindingEdit`, `ObjectBindingSelector`, `DeepPartial` | [`useObjectBindings`](https://react-immersive.liveroom.dev/docs/hooks/use-object-bindings) |
| `ViewerSelectionProps` | [`useViewerSelection`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-selection) |
| `ViewerHoverProps` | [`useViewerHover`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-hover) |
| `ShareableViewerStateProps`, `UseShareableViewerStateOptions` | [`useShareableViewerState`](https://react-immersive.liveroom.dev/docs/hooks/use-shareable-viewer-state) |
| `ViewerModelBounds`, `ViewerModelInfo`, `ViewerModelProps` | [`useViewerModel`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-model) |
| `ViewerActionAnimationController`, `ViewerActionBindingsController`, `ViewerActionCameraController`, `ViewerActionSceneController`, `ViewerActionsOptions`, `ViewerActionsProps` | [`useViewerActions`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-actions) |
| `HomeCameraState`, `ViewerCameraConfig`, `ViewerCameraProps`, `UseViewerCameraOptions` | [`useViewerCamera`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-camera) |
| `ViewerConnectionController`, `ViewerReadyConsumer`, `ViewerReadyHandler` | [`useViewerConnection`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-connection) |
| `AnimationClipInfo`, `AnimationClipState`, `AnimationClipStatus`, `AnimationPlayOptions`, `ViewerAnimationsProps` | [`useViewerAnimations`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-animations) |
| `RuntimeMaterialPatch`, `RuntimeMaterialRanges`, `RuntimePointLightOptions`, `RuntimePointLightPatch`, `RuntimeTransformPatch`, `RuntimeTransformRanges`, `UseViewerEffectsOptions`, `ViewerEffectEasing`, `ViewerEffectsController`, `ViewerEffectRange`, `ViewerEffectTarget`, `ViewerFlickerOptions`, `ViewerLightEffectChannel`, `ViewerMaterialEffectChannel`, `ViewerTimelineOptions`, `ViewerTimelineStep`, `ViewerPulseOptions`, `ViewerTransformEffectChannel`, `ViewerTransitionOptions`, `ViewerVector3` | [`useViewerEffects`](https://react-immersive.liveroom.dev/docs/hooks/use-viewer-effects) |

## `ModelViewerLabels` / `ModelViewerLabelKey`

`ModelViewerLabelKey` is the union of keys in `DEFAULT_MODEL_VIEWER_LABELS`.
`ModelViewerLabels` is `Partial<Record<ModelViewerLabelKey, string>>`, accepted by both viewers' `labels` prop. The default dictionary and both types are exported from the main package, `/simple-model-viewer`, and `/utils` (which can be imported without React).

See the [labels reference](https://react-immersive.liveroom.dev/docs/reference/labels) for examples and the complete catalog.
