# Metabox Basic Configurator API

TypeScript/JavaScript API for integrating Metabox Basic Configurator into web applications. Provides programmatic control over 3D product visualization, materials, environments, and configuration state.

## Table of Contents

- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
  - [NPM Installation (Recommended)](#npm-installation-recommended)
  - [CDN Installation (Quick Prototyping)](#cdn-installation-quick-prototyping)
- [Quick Start](#quick-start)
  - [1. HTML Setup](#1-html-setup)
  - [2. Basic Integration](#2-basic-integration)
    - [JavaScript (ES6 Modules)](#javascript-es6-modules)
    - [TypeScript/Angular/React](#typescriptangularreact)
- [API Reference](#api-reference)
  - [System Terminology](#system-terminology)
  - [Core Integration — `integrateMetabox()`](#core-integration--integratemetabox)
  - [Command Classes](#command-classes)
    - [Product & Environment Management](#product--environment-management)
    - [UI Control](#ui-control)
    - [Camera Control](#camera-control)
    - [Showcase/Animation Control](#showcaseanimation-control)
    - [Measurement Tools](#measurement-tools)
    - [Stream Control](#stream-control)
    - [Export & Media](#export--media)
  - [Event Handling](#event-handling)
    - [Available Events](#available-events)
    - [Event Examples](#event-examples)
  - [Utility Functions](#utility-functions)
- [Standalone Mode](#standalone-mode)
  - [When to Use Standalone Mode](#when-to-use-standalone-mode)
  - [Behavior Changes](#behavior-changes)
  - [Implementation](#implementation)
- [Building a Custom UI](#building-a-custom-ui)
- [Examples](#examples)
- [Troubleshooting](#troubleshooting)
  - [Common Issues](#common-issues)
    - [1. Configurator Not Loading](#1-configurator-not-loading)
    - [2. Commands Not Working](#2-commands-not-working)
- [Tips & Tricks](#tips--tricks)

## Features

- Framework-agnostic integration (Angular, React, Vue, vanilla JS)
- Responsive design support
- Complete control over materials, environments, and products
- Screenshot and PDF export capabilities
- Showcase/animation playback management
- Full TypeScript type definitions
- CDN support for rapid prototyping
- Event-driven architecture for real-time state synchronization

## Prerequisites

**Runtime Requirements:**

- Modern browser with ES6 module support (Chrome 61+, Firefox 60+, Safari 11+, Edge 79+)
- HTTPS connection (required by Unreal Engine streaming)
- Valid Metabox configurator ID

**Development Environment:**

- Node.js >= 20
- npm > 9

**Important Notes:**

- `integrateMetabox()` validates inputs at runtime
- Requires non-empty `configuratorId` and `containerId`
- Iframe URL format: `https://{domain}/metabox-configurator/basic/{configuratorId}`
- Optional query parameters: `introImage`, `introVideo`, `loadingImage`
- HTTPS is strictly enforced (HTTP URLs will be rejected)

## Installation

### NPM Installation (Recommended)

**Installation:**

```bash
npm install @3dsource/metabox-front-api@latest --save
```

**Import:**

```typescript
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, GetPdf, saveImage } from '@3dsource/metabox-front-api';
```

### CDN Installation (Quick Prototyping)

**jsDelivr:**

```javascript
import { integrateMetabox, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';
```

> **Note:** For production, pin a specific version instead of using `@latest`

## Quick Start

### 1. HTML Setup

Ensure your HTML page has the following structure, where the Metabox Configurator will be integrated. You must provide CSS for the #embed3DSource element to make the configurator responsive and fit your layout needs. Create a container element where the configurator will be embedded:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Metabox Configurator</title>
    <style>
      #embed3DSource {
        width: 100%;
        height: 600px;
        border: 1px solid #ccc;
        border-radius: 8px;
      }
    </style>
  </head>
  <body>
    <div id="embed3DSource">
      <!-- Metabox Configurator will be embedded here -->
    </div>
  </body>
</html>
```

### 2. Basic Integration

#### JavaScript (ES6 Modules)

```html
<script type="module">
  import { integrateMetabox, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

  const configuratorId = 'configurator-id'; // your actual configurator id
  // Ensure a container with id embed3DSource exists (see HTML Setup)
  integrateMetabox(configuratorId, 'embed3DSource', (api) => {
    api.addEventListener('configuratorDataUpdated', (env) => {
      console.log('State updated:', env.productId, env.environmentId);
    });
    api.addEventListener('screenshotReady', (image) => {
      if (image) saveImage(image, `configurator-${Date.now()}.png`);
    });
    // Initial commands example
    api.sendCommandToMetabox(new SetProduct('product-1'));
    api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1280, y: 720 }));
  });
</script>
```

#### TypeScript/Angular/React

```typescript
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, SetEnvironment, GetScreenshot, ShowEmbeddedMenu, ShowOverlayInterface, saveImage, type MimeType } from '@3dsource/metabox-front-api';

class MetaboxIntegrator {
  private api: Communicator | null = null;
  private readonly configuratorId: string;

  constructor(configuratorId: string) {
    this.configuratorId = configuratorId;
    this.initialize();
  }

  private initialize(): void {
    integrateMetabox(this.configuratorId, 'embed3DSource', (api: Communicator) => {
      this.api = api;
      this.setupEventListeners();
      this.setupInitialState();
    });
  }

  private setupEventListeners(): void {
    if (!this.api) return;

    // Listen for configuration data changes
    this.api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
      console.log('Configurator data updated:', data);
      this.handleConfiguratorData(data);
    });

    // Handle screenshot events
    this.api.addEventListener('screenshotReady', (imageData: string | null) => {
      saveImage(imageData ?? '', 'configurator-render.png');
    });

    // Handle viewport ready events
    this.api.addEventListener('viewportReady', (isReady: boolean) => {
      console.log('Viewport ready:', isReady);
    });

    // Handle status messages
    this.api.addEventListener('statusMessageChanged', (message: string | null) => {
      console.log('Status message:', message);
    });
  }

  private setupInitialState(): void {
    if (!this.api) return;

    // Configure initial product and environment
    this.api.sendCommandToMetabox(new SetProduct('your-product-id'));
    this.api.sendCommandToMetabox(new SetEnvironment('your-environment-id'));
    this.api.sendCommandToMetabox(new ShowOverlayInterface(true));
    this.api.sendCommandToMetabox(new ShowEmbeddedMenu(true));
  }

  private handleConfiguratorData(data: ConfiguratorEnvelope): void {
    // Access typed data properties
    console.log('Product:', data.productId);
    console.log('Materials:', data.productMaterialsIds);
    console.log('Environment:', data.environmentId);

    // Process configurator data according to your needs
    this.updateUI(data);
  }

  private updateUI(data: ConfiguratorEnvelope): void {
    // Update your application UI based on configurator state
    // This method would contain your specific UI update logic
  }

  // Public API methods
  public takeScreenshot(format: MimeType = 'image/png', size?: { x: number; y: number }): void {
    if (this.api) {
      this.api.sendCommandToMetabox(new GetScreenshot(format, size));
    }
  }

  public changeMaterial(slotId: string, materialId: string): void {
    if (this.api) {
      this.api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
    }
  }

  public changeEnvironment(environmentId: string): void {
    if (this.api) {
      this.api.sendCommandToMetabox(new SetEnvironment(environmentId));
    }
  }

  public destroy(): void {
    this.api = null;
  }
}

// Usage example
const configurator = new MetaboxIntegrator('configurator-id');

// Take a high-resolution screenshot
configurator.takeScreenshot('image/png', { x: 1920, y: 1080 });

// Change material
configurator.changeMaterial('seat-fabric', 'leather-brown');
```

## API Reference

### System Terminology

| Term              | Description                                                 | Context                |
| ----------------- | ----------------------------------------------------------- | ---------------------- |
| **product**       | 3D digital twin of physical product in Metabox system       | Basic configurator     |
| **productId**     | Unique product identifier (system-generated UUID)           | All contexts           |
| **environment**   | 3D scene/room environment for product visualization         | Visual context         |
| **environmentId** | Unique environment identifier (system-generated UUID)       | Environment management |
| **externalId**    | Custom SKU/identifier for integration with external systems | E-commerce integration |
| **showcase**      | Camera animation sequence attached to product               | Product attribute      |
| **slotId**        | Material slot identifier on product/environment             | Material assignment    |
| **materialId**    | Unique material identifier                                  | Material assignment    |

**E-Commerce Configurator (CTA Integration):**

E-Com configurators extend Basic configurators with a call-to-action button. Configuration:

**Configuration:**

1. **CTA Button Label:** Custom text (e.g., "Get Quote", "Add to Cart")
2. **Callback URL:** HTTP POST endpoint that receives configuration data

**CTA Workflow:**

```
User clicks CTA → Metabox POSTs config JSON → Your backend processes → Returns { redirectUrl } → User redirected
```

**Backend Response Format:**

```json
{
  "redirectUrl": "https://your.site/checkout-or-thank-you"
}
```

**Use Cases:**

- Generate PDF quotations
- Create shopping cart entries
- Send lead information to CRM
- Redirect to custom checkout flow

### Core Integration — `integrateMetabox()`

Embeds the Metabox configurator iframe and establishes communication channel.

**Signature:**

```typescript
function integrateMetabox(configuratorId: string, containerId: string, apiReadyCallback: (api: Communicator) => void, config?: IntegrateMetaboxConfig): void;
```

**Parameters:**

| Parameter          | Type                     | Required | Default | Description                        |
| ------------------ | ------------------------ | -------- | ------- | ---------------------------------- |
| `configuratorId`   | `string`                 | ✅       | -       | Configurator UUID (not full URL)   |
| `containerId`      | `string`                 | ✅       | -       | DOM container element ID           |
| `apiReadyCallback` | `function`               | ✅       | -       | Callback invoked when API is ready |
| `config`           | `IntegrateMetaboxConfig` | ❌       | `{}`    | Additional configuration options   |

**Config Options (`IntegrateMetaboxConfig`):**

```typescript
interface IntegrateMetaboxConfig {
  standalone?: boolean; // Disable built-in Metabox UI
  introImage?: string; // URL for intro image
  introVideo?: string; // URL for intro video
  loadingImage?: string; // URL for loading spinner
  state?: string; // Predefined state for initial loading in rison format (see https://github.com/Nanonid/rison)
  domain?: string; // Override domain (HTTPS only)
}
```

**Validation & Errors:**

- Throws if `configuratorId` is empty
- Throws if a container element not found
- Throws if URL is not HTTPS
- Replaces existing iframe with ID `embeddedContent`

**Example:**

```typescript
integrateMetabox(
  'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  'my-container',
  (api) => {
    console.log('Configurator ready');
    api.sendCommandToMetabox(new SetProduct('product-123'));
  },
  {
    standalone: true,
    loadingImage: 'https://cdn.example.com/loader.gif',
  },
);
```

### Command Classes

Commands control configurator behavior. Send via `api.sendCommandToMetabox(new CommandName(...))`.

#### Product & Environment Management

| Command                  | Parameters                           | Description                        |
| ------------------------ | ------------------------------------ | ---------------------------------- |
| `SetProduct`             | `productId: string`                  | Switch to specified product        |
| `SetProductMaterial`     | `slotId: string, materialId: string` | Apply material to product slot     |
| `SetEnvironment`         | `environmentId: string`              | Change environment/scene           |
| `SetEnvironmentMaterial` | `slotId: string, materialId: string` | Apply material to environment slot |

**Example:**

```typescript
api.sendCommandToMetabox(new SetProduct('product-uuid'));
api.sendCommandToMetabox(new SetProductMaterial('body', 'red-metallic'));
```

#### UI Control

- Note: ShowEmbeddedMenu and ShowOverlayInterface commands are ineffective in standalone mode.

| Command                | Parameters         | Description                 |
| ---------------------- | ------------------ | --------------------------- |
| `ShowEmbeddedMenu`     | `visible: boolean` | Show/hide right sidebar     |
| `ShowOverlayInterface` | `visible: boolean` | Show/hide viewport controls |

**Example:**

```typescript
api.sendCommandToMetabox(new ShowEmbeddedMenu(false)); // Hide for custom UI
```

#### Camera Control

| Command       | Parameters                     | Description                                                 |
| ------------- | ------------------------------ | ----------------------------------------------------------- |
| `SetCamera`   | `camera: CameraCommandPayload` | Set camera position, rotation, fov and restrictions         |
| `GetCamera`   | None                           | Request current camera; returns via `getCameraResult` event |
| `ResetCamera` | None                           | Reset camera to default position                            |
| `ApplyZoom`   | `zoom: number`                 | Set zoom level (within configured limits)                   |

**Example:**

```typescript
api.sendCommandToMetabox(new ApplyZoom(50)); // Zoom in
api.sendCommandToMetabox(new ApplyZoom(-25)); // Zoom out
api.sendCommandToMetabox(new ResetCamera()); // Reset view
api.sendCommandToMetabox(
  new SetCamera({
    fov: 45,
    mode: 'orbit',
    position: { x: 100, y: 100, z: 100 },
    rotation: { horizontal: 0, vertical: 0 },
    restrictions: {
      maxDistanceToPivot: 0,
      maxFov: 0,
      maxHorizontalRotation: 0,
      maxVerticalRotation: 0,
      minDistanceToPivot: 0,
      minFov: 0,
      minHorizontalRotation: 0,
      minVerticalRotation: 0,
    },
  }),
);

// Request current camera state
api.sendCommandToMetabox(new GetCamera());

// Listen for the camera data
api.addEventListener('getCameraResult', (camera) => {
  console.log('Current camera:', camera);
  // camera is of type CameraCommandPayload
});
```

##### `CameraCommandPayload`

The payload used by the `SetCamera` command to configure the camera.

```typescript
interface CameraCommandPayload {
  fov?: number;
  mode?: 'fps' | 'orbit';
  position?: { x: number; y: number; z: number };
  rotation?: {
    horizontal: number;
    vertical: number;
  };
  restrictions?: {
    maxDistanceToPivot?: number;
    maxFov?: number;
    maxHorizontalRotation?: number;
    maxVerticalRotation?: number;
    minDistanceToPivot?: number;
    minFov?: number;
    minHorizontalRotation?: number;
    minVerticalRotation?: number;
  };
}
```

- `fov` (number): Field of view in degrees.
- `mode` ('fps' | 'orbit'):
  - `fps`: First‑person style camera, moves freely in space.
  - `orbit`: Orbiting camera around a pivot (typical product viewer behavior).
- `position` ({ x, y, z }): Camera position in world coordinates.
- `rotation` ({ horizontal, vertical }): Camera rotation angles in degrees.
  - `horizontal`: Yaw (left/right).
  - `vertical`: Pitch (up/down).
- `restrictions` (optional): Limits applied by the viewer to constrain user/camera movement.
  - Distance limits (orbit mode): `minDistanceToPivot`, `maxDistanceToPivot`.
  - Field‑of‑view limits (first-person mode): `minFov`, `maxFov`.
  - Rotation limits: `minHorizontalRotation`, `maxHorizontalRotation`, `minVerticalRotation`, `maxVerticalRotation`.

Notes:

- Provide only the limits you want to enforce; unspecified values are left as currently configured by the viewer.
- Rotation units are degrees; positive/negative values follow the viewer’s right‑handed coordinate system.
- In `orbit` mode, distance limits are interpreted relative to the orbit pivot.
- `ResetCamera` restores the default position/rotation/FOV defined by the current product or environment template.

#### Showcase/Animation Control

| Command         | Parameters | Description                          |
| --------------- | ---------- | ------------------------------------ |
| `InitShowcase`  | None       | Initialize showcase (load animation) |
| `PlayShowcase`  | None       | Start/resume animation playback      |
| `PauseShowcase` | None       | Pause animation                      |
| `StopShowcase`  | None       | Stop and reset animation             |

#### Measurement Tools

| Command           | Parameters | Description                |
| ----------------- | ---------- | -------------------------- |
| `ShowMeasurement` | None       | Display product dimensions |
| `HideMeasurement` | None       | Hide dimension overlay     |

#### Stream Control

| Command        | Parameters | Description                                                 |
| -------------- | ---------- | ----------------------------------------------------------- |
| `ResumeStream` | None       | Resume pixel streaming session after pause or disconnection |

**Example:**

```typescript
// Resume the stream after an idle timeout or network interruption
api.sendCommandToMetabox(new ResumeStream());
```

#### Export & Media

| Command                      | Parameters                                          | Description          | Event Triggered        |
| ---------------------------- | --------------------------------------------------- | -------------------- | ---------------------- |
| `GetScreenshot`              | `mimeType: MimeType, size?: {x: number, y: number}` | Render screenshot    | `screenshotReady`      |
| `GetPdf`                     | None                                                | Generate PDF export  | Server-side (no event) |
| `GetCallToActionInformation` | None                                                | Trigger CTA workflow | Backend redirect       |

**Screenshot Example:**

```typescript
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));

api.addEventListener('screenshotReady', (imageData) => {
  saveImage(imageData, 'product.png');
});
```

### Event Handling

Event-driven architecture for reactive state management. Register listeners via `api.addEventListener(eventName, handler)`.

#### Available Events

| Event                         | Payload Type           | Description                  | Use Case                                  |
| ----------------------------- | ---------------------- | ---------------------------- | ----------------------------------------- |
| `configuratorDataUpdated`     | `ConfiguratorEnvelope` | Configuration state changed  | Sync UI with product/material/environment |
| `ecomConfiguratorDataUpdated` | `EcomConfigurator`     | CTA configuration loaded     | Display CTA button with label             |
| `viewportReady`               | `boolean`              | 3D viewport ready state      | Hide loading, enable interactions         |
| `showcaseStatusChanged`       | `ShowCaseStatus`       | Animation playback status    | Update play/pause button state            |
| `statusMessageChanged`        | `string \| null`       | Loading/progress message     | Display user feedback                     |
| `screenshotReady`             | `string \| null`       | Base64 screenshot data       | Download or display image                 |
| `getCameraResult`             | `CameraCommandPayload` | Current camera data returned | Capture/store current camera              |
| `videoResolutionChanged`      | `VideoResolution`      | Stream resolution changed    | Adjust viewport layout                    |

#### Event Examples

**Configuration State Sync:**

```typescript
api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
  console.log('Product:', data.productId);
  console.log('Materials:', data.productMaterialsIds);
  console.log('Environment:', data.environmentId);

  // Update custom UI
  updateProductSelector(data.productId);
  updateMaterialSwatches(data.productMaterialsIds);
});
```

**Screenshot Handling:**

```typescript
api.addEventListener('screenshotReady', (imageData: string | null) => {
  if (!imageData) {
    console.error('Screenshot failed');
    return;
  }

  saveImage(imageData, `product-${Date.now()}.png`);
});
```

**Loading State:**

```typescript
api.addEventListener('viewportReady', (isReady: boolean) => {
  if (isReady) {
    hideLoadingSpinner();
    enableComponentSelectors();
  }
});

api.addEventListener('statusMessageChanged', (message: string | null) => {
  document.getElementById('status-text').textContent = message || '';
});
```

**Showcase Control:**

```typescript
api.addEventListener('showcaseStatusChanged', (status: ShowCaseStatus) => {
  const playButton = document.getElementById('play-btn');

  switch (status) {
    case 'play':
      playButton.textContent = 'Pause';
      break;
    case 'pause':
    case 'stop':
      playButton.textContent = 'Play';
      break;
  }
});
```

### Utility Functions

#### `saveImage(imageUrl: string, filename: string): void`

Downloads base64-encoded image to user's device.

**Parameters:**

| Parameter  | Type     | Description                                         |
| ---------- | -------- | --------------------------------------------------- |
| `imageUrl` | `string` | Base64 data URL (e.g., `data:image/png;base64,...`) |
| `filename` | `string` | Desired filename with extension                     |

**Example:**

```typescript
api.addEventListener('screenshotReady', (imageData: string | null) => {
  if (imageData) {
    saveImage(imageData, `config-${Date.now()}.png`);
  }
});
```

**Implementation Note:**
Creates a temporary anchor element with `download` attribute to trigger browser download.

#### `fromCommunicatorEvent(target: Communicator, eventName: string): Observable`

RxJS wrapper for Communicator events. Works like RxJS `fromEvent` — creates a typed Observable that emits each time the specified event fires. Alternative to `api.addEventListener()`.

**Parameters:**

| Parameter   | Type           | Description                                  |
| ----------- | -------------- | -------------------------------------------- |
| `target`    | `Communicator` | The Communicator instance to listen on       |
| `eventName` | `string`       | Event name (e.g., `configuratorDataUpdated`) |

**Returns:** `Observable<T>` — typed Observable matching the event payload.

**Example:**

```typescript
import { fromCommunicatorEvent } from '@3dsource/metabox-front-api';

integrateMetabox('configurator-id', 'embed3DSource', (api) => {
  fromCommunicatorEvent(api, 'configuratorDataUpdated').subscribe((data) => {
    console.log('Product:', data.productId);
  });

  fromCommunicatorEvent(api, 'screenshotReady').subscribe((imageData) => {
    if (imageData) saveImage(imageData, 'screenshot.png');
  });
});
```

## Standalone Mode

Headless rendering mode for custom UI implementations. Disables all built-in Metabox UI elements.

### When to Use Standalone Mode

✅ **Use When:**

- Building a fully custom configurator interface
- Implementing brand-specific product selection UX
- Integrating into existing design systems
- Requiring complete control over configuration flow

❌ **Don't Use When:**

- Quick prototyping with default UI is sufficient
- Minimal customization needed
- Limited development resources

### Behavior Changes

| Feature            | Default Mode | Standalone Mode |
| ------------------ | ------------ | --------------- |
| Right sidebar menu | Visible      | Hidden          |
| Viewport overlays  | Visible      | Hidden          |
| Template logic     | Active       | Disabled        |
| API control        | Partial      | Full            |
| Event system       | Available    | Available       |

### Implementation

**TypeScript:**

```typescript
import { integrateMetabox, Communicator, SetProduct, SetEnvironment } from '@3dsource/metabox-front-api';

integrateMetabox(
  'configurator-id',
  'embed3DSource',
  (api: Communicator) => {
    // Work with API here
  },
  { standalone: true }, // <- Enable standalone mode to disable default UI
);
```

**JavaScript (CDN):**

```html
<script type="module">
  import { integrateMetabox } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

  integrateMetabox(
    'configurator-id',
    'embed3DSource',
    (api) => {
      // Work with API here
    },
    { standalone: true }, // <- Enable standalone mode to disable default UI
  );
</script>
```

## Building a Custom UI

To create a custom configurator interface, use [standalone mode](#standalone-mode) and subscribe to events before sending commands, then follow these steps:

**1. Listen to State Changes**

```typescript
api.addEventListener('configuratorDataUpdated', (data) => {
  // Update your UI based on configurator state
});
```

**2. Control Product Configuration**

```typescript
api.sendCommandToMetabox(new SetProduct('product-id')); // Change product
api.sendCommandToMetabox(new SetProductMaterial('slot-id', 'material-id')); // Apply material
api.sendCommandToMetabox(new SetEnvironment('environment-id')); // Change environment
```

**3. Export Configuration**

```typescript
api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
api.sendCommandToMetabox(new GetPdf());
api.sendCommandToMetabox(new GetCallToActionInformation());
```

## Examples

### E‑Com CTA example

Minimal example of triggering the Call‑To‑Action flow from your page once the API is ready.

```html
<div id="metabox-container"></div>
<button id="cta-btn" disabled>Send configuration (CTA)</button>

<script type="module">
  import { integrateMetabox, GetCallToActionInformation } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

  let api = null;

  integrateMetabox('configurator-id', 'metabox-container', (apiInstance) => {
    api = apiInstance;
    document.getElementById('cta-btn').disabled = false;
  });

  document.getElementById('cta-btn').addEventListener('click', () => {
    // Triggers sending the current configuration to your CTA Callback URL
    api.sendCommandToMetabox(new GetCallToActionInformation());
  });
</script>
```

### Basic Product Configurator

A complete HTML example showing how to integrate the Metabox configurator with vanilla JavaScript.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Product Configurator</title>
    <style>
      #metabox-container {
        width: 100%;
        height: 600px;
        border: 1px solid #ddd;
        border-radius: 8px;
      }

      .controls {
        margin: 20px 0;
      }

      button {
        margin: 5px;
        padding: 10px 15px;
        border: 1px solid #ccc;
        border-radius: 4px;
        background: #f5f5f5;
        cursor: pointer;
      }

      button:hover {
        background: #e5e5e5;
      }

      button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      .loading {
        text-align: center;
        padding: 20px;
        color: #666;
      }

      .error {
        color: #d32f2f;
        padding: 10px;
        background: #ffebee;
        border: 1px solid #ffcdd2;
        border-radius: 4px;
        margin: 10px 0;
      }
    </style>
  </head>
  <body>
    <div id="loading" class="loading">Loading configurator...</div>
    <div id="error" class="error" style="display: none;"></div>
    <div id="metabox-container"></div>
    <div class="controls">
      <button id="product1-btn" onclick="changeProduct('product-1')" disabled>Product 1</button>
      <button id="product2-btn" onclick="changeProduct('product-2')" disabled>Product 2</button>
      <button id="red-material-btn" onclick="applyMaterial('slot-1', 'material-red')" disabled>Red Material</button>
      <button id="blue-material-btn" onclick="applyMaterial('slot-1', 'material-blue')" disabled>Blue Material</button>
      <button id="screenshot-btn" onclick="takeScreenshot()" disabled>Take Screenshot</button>
    </div>

    <script type="module">
      import { integrateMetabox, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from 'https://cdn.jsdelivr.net/npm/@3dsource/metabox-front-api@latest/+esm';

      let api = null;
      let isApiReady = false;

      // Helper function to show/hide loading state
      function setLoadingState(loading) {
        const loadingEl = document.getElementById('loading');
        const buttons = document.querySelectorAll('button');

        loadingEl.style.display = loading ? 'block' : 'none';
        buttons.forEach((btn) => (btn.disabled = loading || !isApiReady));
      }

      // Helper function to show error messages
      function showError(message) {
        const errorEl = document.getElementById('error');
        errorEl.textContent = message;
        errorEl.style.display = 'block';
        setLoadingState(false);
      }

      // Initialize configurator with error handling
      try {
        // Replace 'configurator-id' with your actual configurator ID from 3DSource
        const configuratorId = 'configurator-id';

        integrateMetabox(configuratorId, 'metabox-container', (apiInstance) => {
          try {
            api = apiInstance;
            isApiReady = true;
            setLoadingState(false);
            console.log('Configurator ready!');

            // Listen for configurator state changes
            api.addEventListener('configuratorDataUpdated', (data) => {
              console.log('Configurator state updated:', data);
              // You can update your UI based on the current state
              // For example, update available materials, products, etc.
            });

            // Listen for screenshot completion
            api.addEventListener('screenshotReady', (imageData) => {
              console.log('Screenshot captured successfully');
              // Save the image with a timestamp
              saveImage(imageData, `configurator-screenshot-${Date.now()}.png`);
            });
          } catch (error) {
            console.error('Error setting up API:', error);
            showError('Failed to initialize configurator API: ' + error.message);
          }
        });
      } catch (error) {
        console.error('Error initializing configurator:', error);
        showError('Failed to load configurator: ' + error.message);
      }

      // Global functions for button interactions
      window.changeProduct = (productId) => {
        if (!isApiReady || !api) {
          console.warn('API not ready yet');
          return;
        }

        try {
          console.log(`Changing to product: ${productId}`);
          api.sendCommandToMetabox(new SetProduct(productId));
        } catch (error) {
          console.error('Error changing product:', error);
          showError('Failed to change product: ' + error.message);
        }
      };

      window.applyMaterial = (slotId, materialId) => {
        if (!isApiReady || !api) {
          console.warn('API not ready yet');
          return;
        }

        try {
          console.log(`Applying material ${materialId} to slot ${slotId}`);
          api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
        } catch (error) {
          console.error('Error applying material:', error);
          showError('Failed to apply material: ' + error.message);
        }
      };

      window.takeScreenshot = () => {
        if (!isApiReady || !api) {
          console.warn('API not ready yet');
          return;
        }

        try {
          console.log('Taking screenshot...');
          // Request high-quality screenshot in PNG format
          api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
        } catch (error) {
          console.error('Error taking screenshot:', error);
          showError('Failed to take screenshot: ' + error.message);
        }
      };
    </script>
  </body>
</html>
```

### React Integration Example

```tsx
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { integrateMetabox, Communicator, ConfiguratorEnvelope, SetProduct, SetProductMaterial, GetScreenshot, saveImage } from '@3dsource/metabox-front-api';

interface MetaboxConfiguratorProps {
  configuratorId: string;
  onStateChange?: (data: ConfiguratorEnvelope) => void;
  onError?: (error: string) => void;
  className?: string;
}

interface ConfiguratorState {
  isLoading: boolean;
  error: string | null;
  isApiReady: boolean;
}

const MetaboxConfigurator: React.FC<MetaboxConfiguratorProps> = ({ configuratorId, onStateChange, onError, className }) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [api, setApi] = useState<Communicator | null>(null);
  const [state, setState] = useState<ConfiguratorState>({
    isLoading: true,
    error: null,
    isApiReady: false,
  });

  // Generate unique container ID to avoid conflicts
  const containerId = useRef(`metabox-container-${Math.random().toString(36).substr(2, 9)}`);

  // Error handler
  const handleError = useCallback(
    (error: string) => {
      setState((prev) => ({ ...prev, error, isLoading: false }));
      onError?.(error);
      console.error('Metabox Configurator Error:', error);
    },
    [onError],
  );

  // Initialize configurator
  useEffect(() => {
    if (!containerRef.current) return;

    let mounted = true;

    const initializeConfigurator = async () => {
      try {
        setState((prev) => ({ ...prev, isLoading: true, error: null }));

        // Set the container ID
        containerRef.current!.id = containerId.current;

        integrateMetabox(configuratorId, containerId.current, (apiInstance) => {
          if (!mounted) return; // Component was unmounted

          try {
            setApi(apiInstance);
            setState((prev) => ({ ...prev, isLoading: false, isApiReady: true }));

            // Set up event listeners with proper typing
            apiInstance.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
              if (mounted) {
                onStateChange?.(data);
              }
            });

            apiInstance.addEventListener('screenshotReady', (imageData: string) => {
              if (mounted) {
                console.log('Screenshot captured successfully');
                saveImage(imageData, `configurator-screenshot-${Date.now()}.png`);
              }
            });

            console.log('React Metabox Configurator initialized successfully');
          } catch (error) {
            if (mounted) {
              handleError(`Failed to set up API: ${error instanceof Error ? error.message : String(error)}`);
            }
          }
        });
      } catch (error) {
        if (mounted) {
          handleError(`Failed to initialize configurator: ${error instanceof Error ? error.message : String(error)}`);
        }
      }
    };

    initializeConfigurator();

    // Cleanup function
    return () => {
      mounted = false;
      if (api) {
        // Clean up any event listeners if the API provides cleanup methods
        console.log('Cleaning up Metabox Configurator');
      }
    };
  }, [configuratorId, onStateChange, handleError, api]);

  // Command methods with error handling
  const changeProduct = useCallback(
    (productId: string) => {
      if (!state.isApiReady || !api) {
        console.warn('API not ready yet');
        return;
      }

      try {
        console.log(`Changing to product: ${productId}`);
        api.sendCommandToMetabox(new SetProduct(productId));
      } catch (error) {
        handleError(`Failed to change product: ${error instanceof Error ? error.message : String(error)}`);
      }
    },
    [api, state.isApiReady, handleError],
  );

  const applyMaterial = useCallback(
    (slotId: string, materialId: string) => {
      if (!state.isApiReady || !api) {
        console.warn('API not ready yet');
        return;
      }

      try {
        console.log(`Applying material ${materialId} to slot ${slotId}`);
        api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
      } catch (error) {
        handleError(`Failed to apply material: ${error instanceof Error ? error.message : String(error)}`);
      }
    },
    [api, state.isApiReady, handleError],
  );

  const takeScreenshot = useCallback(() => {
    if (!state.isApiReady || !api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log('Taking screenshot...');
      api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
    } catch (error) {
      handleError(`Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`);
    }
  }, [api, state.isApiReady, handleError]);

  // Render error state
  if (state.error) {
    return (
      <div className={className}>
        <div
          style={{
            color: '#d32f2f',
            padding: '20px',
            background: '#ffebee',
            border: '1px solid #ffcdd2',
            borderRadius: '4px',
            textAlign: 'center',
          }}
        >
          <h3>Configurator Error</h3>
          <p>{state.error}</p>
          <button
            onClick={() => window.location.reload()}
            style={{
              padding: '10px 20px',
              marginTop: '10px',
              border: '1px solid #d32f2f',
              background: '#fff',
              color: '#d32f2f',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            Reload Page
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className={className}>
      {/* Loading indicator */}
      {state.isLoading && (
        <div
          style={{
            textAlign: 'center',
            padding: '40px',
            color: '#666',
            background: '#f5f5f5',
            borderRadius: '8px',
          }}
        >
          <div>Loading configurator...</div>
          <div style={{ marginTop: '10px', fontSize: '14px' }}>Please wait while we initialize the 3D viewer</div>
        </div>
      )}

      {/* Configurator container */}
      <div
        ref={containerRef}
        style={{
          width: '100%',
          height: '600px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          display: state.isLoading ? 'none' : 'block',
        }}
      />

      {/* Controls */}
      {state.isApiReady && (
        <div style={{ marginTop: '20px' }}>
          <div style={{ marginBottom: '10px', fontWeight: 'bold', color: '#333' }}>Configurator Controls:</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
            <button
              onClick={() => changeProduct('product-1')}
              disabled={!state.isApiReady}
              style={{
                padding: '10px 15px',
                border: '1px solid #ccc',
                borderRadius: '4px',
                background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
                cursor: state.isApiReady ? 'pointer' : 'not-allowed',
                transition: 'background-color 0.2s',
              }}
              onMouseOver={(e) => {
                if (state.isApiReady) e.currentTarget.style.background = '#e5e5e5';
              }}
              onMouseOut={(e) => {
                if (state.isApiReady) e.currentTarget.style.background = '#f5f5f5';
              }}
            >
              Product 1
            </button>
            <button
              onClick={() => changeProduct('product-2')}
              disabled={!state.isApiReady}
              style={{
                padding: '10px 15px',
                border: '1px solid #ccc',
                borderRadius: '4px',
                background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
                cursor: state.isApiReady ? 'pointer' : 'not-allowed',
              }}
            >
              Product 2
            </button>
            <button
              onClick={() => applyMaterial('slot-1', 'material-red')}
              disabled={!state.isApiReady}
              style={{
                padding: '10px 15px',
                border: '1px solid #ccc',
                borderRadius: '4px',
                background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
                cursor: state.isApiReady ? 'pointer' : 'not-allowed',
              }}
            >
              Red Material
            </button>
            <button
              onClick={() => applyMaterial('slot-1', 'material-blue')}
              disabled={!state.isApiReady}
              style={{
                padding: '10px 15px',
                border: '1px solid #ccc',
                borderRadius: '4px',
                background: state.isApiReady ? '#f5f5f5' : '#e0e0e0',
                cursor: state.isApiReady ? 'pointer' : 'not-allowed',
              }}
            >
              Blue Material
            </button>
            <button
              onClick={takeScreenshot}
              disabled={!state.isApiReady}
              style={{
                padding: '10px 15px',
                border: '1px solid #007bff',
                borderRadius: '4px',
                background: state.isApiReady ? '#007bff' : '#6c757d',
                color: 'white',
                cursor: state.isApiReady ? 'pointer' : 'not-allowed',
              }}
            >
              📸 Take Screenshot
            </button>
          </div>
        </div>
      )}
    </div>
  );
};

export default MetaboxConfigurator;
```

### Angular Integration Example

```typescript
// metabox-configurator.component.ts
import { Component, input, OnInit, output, signal } from '@angular/core';
import { Communicator, ConfiguratorEnvelope, GetCallToActionInformation, GetPdf, GetScreenshot, InitShowcase, integrateMetabox, PauseShowcase, PlayShowcase, saveImage, SetProductMaterial, SetProduct, ShowEmbeddedMenu, ShowOverlayInterface, StopShowcase } from '@3dsource/metabox-front-api';

interface ConfiguratorState {
  isLoading: boolean;
  error: string | null;
  isApiReady: boolean;
}

@Component({
  selector: 'app-metabox-configurator',
  template: `
    @let _state = state();
    <div class="configurator-container">
      <!-- Loading State -->
      @if (_state.isLoading) {
        <div class="loading-container">
          <div class="loading-content">
            <div class="loading-spinner"></div>
            <div class="loading-text">Loading configurator...</div>
            <div class="loading-subtext">Please wait while we initialize the 3D viewer</div>
          </div>
        </div>
      }

      <!-- Error State -->
      @if (_state.error) {
        <div class="error-container">
          <h3>Configurator Error</h3>
          <p>{{ _state.error }}</p>
          <button (click)="retryInitialization()" class="retry-button">Retry</button>
        </div>
      }

      <!-- Configurator Container -->
      <div [id]="containerId()" class="configurator-viewport" [hidden]="_state.isLoading || _state.error"></div>

      <!-- Controls -->
      @if (_state.isApiReady) {
        <div class="controls">
          <div class="controls-title">Configurator Controls:</div>
          <div class="controls-buttons">
            <button (click)="changeProduct('541f46ab-a86c-48e3-bcfa-f92341483db3')" [disabled]="!_state.isApiReady" class="control-button">Product Change</button>
            <button (click)="initShowcase()" [disabled]="!_state.isApiReady" class="control-button">Init showcase</button>
            <button (click)="stopShowcase()" [disabled]="!_state.isApiReady" class="control-button">Stop showcase</button>
            <button (click)="playShowcase()" [disabled]="!_state.isApiReady" class="control-button">Play showcase</button>
            <button (click)="pauseShowcase()" [disabled]="!_state.isApiReady" class="control-button">Pause showcase</button>
            <button (click)="applyMaterial('slot-1', 'material-red')" [disabled]="!_state.isApiReady" class="control-button">Red Material</button>
            <button (click)="applyMaterial('slot-1', 'material-blue')" [disabled]="!_state.isApiReady" class="control-button">Blue Material</button>
            <button (click)="getPdf()" [disabled]="!_state.isApiReady" class="control-button">Get PDF</button>
            <button (click)="takeScreenshot()" [disabled]="!_state.isApiReady" class="control-button screenshot-button">📸 Take Screenshot</button>
            <button (click)="sendCallToActionInformation()" [disabled]="!_state.isApiReady" class="control-button">Send Call To Action Information</button>
          </div>
        </div>
      }
    </div>
  `,
  styles: [
    `
      .configurator-container {
        width: 100%;
        position: relative;
      }

      .configurator-viewport {
        width: 100%;
        height: 600px;
        border: 1px solid #ddd;
        border-radius: 8px;
      }

      .loading-container {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 600px;
        background: #f5f5f5;
        border: 1px solid #ddd;
        border-radius: 8px;
      }

      .loading-content {
        text-align: center;
        color: #666;
      }

      .loading-spinner {
        width: 40px;
        height: 40px;
        border: 4px solid #f3f3f3;
        border-top: 4px solid #007bff;
        border-radius: 50%;
        animation: spin 1s linear infinite;
        margin: 0 auto 20px;
      }

      @keyframes spin {
        0% {
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(360deg);
        }
      }

      .loading-text {
        font-size: 16px;
        font-weight: 500;
        margin-bottom: 8px;
      }

      .loading-subtext {
        font-size: 14px;
        opacity: 0.8;
      }

      .error-container {
        padding: 40px;
        text-align: center;
        background: #ffebee;
        border: 1px solid #ffcdd2;
        border-radius: 8px;
        color: #d32f2f;
      }

      .error-container h3 {
        margin: 0 0 16px 0;
        font-size: 18px;
      }

      .error-container p {
        margin: 0 0 20px 0;
        font-size: 14px;
      }

      .retry-button {
        padding: 10px 20px;
        border: 1px solid #d32f2f;
        background: #fff;
        color: #d32f2f;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
      }

      .retry-button:hover {
        background: #d32f2f;
        color: #fff;
      }

      .controls {
        margin-top: 20px;
      }

      .controls-title {
        font-weight: bold;
        color: #333;
        margin-bottom: 10px;
      }

      .controls-buttons {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
      }

      .control-button {
        padding: 10px 15px;
        border: 1px solid #ccc;
        border-radius: 4px;
        background: #f5f5f5;
        cursor: pointer;
        transition: all 0.2s;
        font-size: 14px;
      }

      .control-button:hover:not(:disabled) {
        background: #e5e5e5;
      }

      .control-button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      .screenshot-button {
        border-color: #007bff;
        background: #007bff;
        color: white;
      }

      .screenshot-button:hover:not(:disabled) {
        background: #0056b3;
      }
    `,
  ],
})
export class MetaboxConfiguratorComponent implements OnInit {
  configuratorId = input.required<string>();
  stateChange = output<ConfiguratorEnvelope>();
  errorFired = output<string>();
  state = signal<ConfiguratorState>({
    isLoading: true,
    error: null,
    isApiReady: false,
  });
  containerId = signal(`metabox-container-${Math.random().toString(36).substring(2, 9)}`);
  private api: Communicator | null = null;

  ngOnInit(): void {
    this.initializeConfigurator();
  }

  // Public methods for external control
  changeProduct(productId: string): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log(`Changing to product: ${productId}`);
      this.api.sendCommandToMetabox(new SetProduct(productId));
    } catch (error) {
      this.handleError(`Failed to change product: ${this.getErrorMessage(error)}`);
    }
  }

  initShowcase(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      this.api.sendCommandToMetabox(new InitShowcase());
    } catch (error) {
      this.handleError(`Failed to init showcase for product: ${this.getErrorMessage(error)}`);
    }
  }

  stopShowcase(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log(`Stop showcase`);
      this.api.sendCommandToMetabox(new StopShowcase());
    } catch (error) {
      this.handleError(`Failed to init showcase for product: ${this.getErrorMessage(error)}`);
    }
  }

  playShowcase(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log(`Play showcase`);
      this.api.sendCommandToMetabox(new PlayShowcase());
    } catch {
      this.handleError(`Failed to play showcase`);
    }
  }

  pauseShowcase(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log(`Pause showcase`);
      this.api.sendCommandToMetabox(new PauseShowcase());
    } catch {
      this.handleError(`Failed to pause showcase`);
    }
  }

  applyMaterial(slotId: string, materialId: string): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log(`Applying material ${materialId} to slot ${slotId}`);
      this.api.sendCommandToMetabox(new SetProductMaterial(slotId, materialId));
    } catch (error) {
      this.handleError(`Failed to apply material: ${this.getErrorMessage(error)}`);
    }
  }

  takeScreenshot(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log('Taking screenshot...');
      this.api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1920, y: 1080 }));
    } catch (error) {
      this.handleError(`Failed to take screenshot: ${this.getErrorMessage(error)}`);
    }
  }

  sendCallToActionInformation(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }
    try {
      console.log('Generating cta information...');
      this.api.sendCommandToMetabox(new GetCallToActionInformation());
    } catch (error) {
      this.handleError(`Failed to generating cta information: ${this.getErrorMessage(error)}`);
    }
  }

  getPdf(): void {
    if (!this.state().isApiReady || !this.api) {
      console.warn('API not ready yet');
      return;
    }

    try {
      console.log('Generating PDF...');
      this.api.sendCommandToMetabox(new GetPdf());
    } catch (error) {
      this.handleError(`Failed to generating pdf: ${this.getErrorMessage(error)}`);
    }
  }

  retryInitialization(): void {
    this.updateState({ error: null });
    this.initializeConfigurator();
  }

  sendInitCommands() {
    if (!this.api) {
      return;
    }

    this.api.sendCommandToMetabox(new ShowEmbeddedMenu(true));
    this.api.sendCommandToMetabox(new ShowOverlayInterface(true));
  }

  private initializeConfigurator() {
    try {
      this.updateState({ isLoading: true, error: null });

      integrateMetabox(this.configuratorId(), this.containerId(), (apiInstance) => {
        try {
          this.api = apiInstance;
          this.updateState({ isLoading: false, isApiReady: true });
          this.sendInitCommands();
          this.setupEventListeners();

          console.log('Angular Metabox Configurator initialized successfully');
        } catch (error) {
          this.handleError(`Failed to set up API: ${this.getErrorMessage(error)}`);
        }
      });
    } catch (error) {
      this.handleError(`Failed to initialize configurator: ${this.getErrorMessage(error)}`);
    }
  }

  private setupEventListeners(): void {
    if (!this.api) {
      return;
    }
    // Listen for configurator state changes
    this.api.addEventListener('configuratorDataUpdated', (data: ConfiguratorEnvelope) => {
      console.log('Configurator state updated:', data);
      this.stateChange.emit(data);
    });

    // Listen for screenshot completion
    this.api.addEventListener('screenshotReady', (imageData: string | null) => {
      console.log(`Screenshot captured successfully ${imageData ?? ''}`);
      saveImage(imageData ?? '', `configurator-screenshot-${Date.now()}.png`);
    });
  }

  private updateState(updates: Partial<ConfiguratorState>): void {
    this.state.set({ ...this.state(), ...updates });
  }

  private handleError(errorMessage: string): void {
    console.error('Metabox Configurator Error:', errorMessage);
    this.updateState({ error: errorMessage, isLoading: false });
    this.errorFired.emit(errorMessage);
  }

  private getErrorMessage(error: any): string {
    if (error instanceof Error) {
      return error.message;
    }
    return String(error);
  }
}
```

**Key Features of this Angular Example:**

- **Comprehensive Error Handling**: Try-catch blocks and user-friendly error states
- **Loading States**: Visual feedback with spinner during initialization
- **TypeScript Integration**: Full type safety with proper interfaces
- **Unique Container IDs**: Automatic ID generation to avoid conflicts
- **Event Outputs**: Emits state changes and errors to parent components
- **Responsive Design**: Clean, accessible button layout with proper styling

**Usage Example:**

```typescript
// app.component.ts
import { Component } from '@angular/core';
import { ConfiguratorEnvelope } from '@3dsource/metabox-front-api';

@Component({
  selector: 'app-root',
  template: `
    <div class="app-container">
      <h1>My Product Configurator</h1>
      <app-metabox-configurator [configuratorId]="configuratorId" (stateChange)="onConfiguratorStateChange($event)" (errorFired)="onConfiguratorError($event)"></app-metabox-configurator>
    </div>
  `,
  styles: [
    `
      .app-container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }
    `,
  ],
})
export class AppComponent {
  configuratorId = 'configurator-id';

  onConfiguratorStateChange(data: ConfiguratorEnvelope): void {
    console.log('Configurator state changed:', data);
    // Update your application state based on configurator changes
  }

  onConfiguratorError(error: string): void {
    console.error('Configurator error:', error);
    // Handle errors (show notifications, log to analytics, etc.)
  }
}
```

**To Use This Component:**

1. Install the package: `npm install @3dsource/metabox-front-api@latest`
2. Import the component in your Angular module
3. Replace `'configurator-id'` with your actual configurator ID
4. Replace placeholder IDs with your real product, material, and slot IDs
5. Customize the styling by modifying the component styles

## Troubleshooting

### Common Issues

#### 1. Configurator Not Loading

**Symptoms:** Blank screen, iframe not appearing, loading indefinitely

**Root Causes & Solutions:**

| Issue                       | Diagnostic                           | Solution                                                  |
| --------------------------- | ------------------------------------ | --------------------------------------------------------- |
| Invalid configurator ID     | Browser console shows 404 errors     | Verify configurator ID in Metabox admin                   |
| Container not found         | `Error: Container element not found` | Ensure element exists before calling `integrateMetabox()` |
| HTTP instead of HTTPS       | Mixed content warnings               | Use HTTPS or test on localhost                            |
| Container has no dimensions | Invisible iframe (0x0)               | Set explicit width/height on container element            |
| CORS/iframe blocking        | X-Frame-Options errors               | Check domain allowlist in Metabox settings                |

**Diagnostic Code:**

```typescript
const containerId = 'embed3DSource';
const container = document.getElementById(containerId);

if (!container) {
  throw new Error(`Container #${containerId} not found`);
}

// Verify container has dimensions
const rect = container.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
  console.warn('⚠️ Container has no dimensions');
}

if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
  console.warn('⚠️ HTTPS required for Unreal Engine streaming');
}

integrateMetabox('configurator-id', containerId, (api) => {
  console.log('✅ Configurator loaded');
});
```

#### 2. Commands Not Working

**Symptoms:** Commands sent but no visual changes in configurator

**Common Mistakes:**

| Mistake                    | Problem           | Solution                                        |
| -------------------------- | ----------------- | ----------------------------------------------- |
| Sending before API ready   | Commands ignored  | Only send in `apiReadyCallback`                 |
| Wrong product/material IDs | Entity not found  | Verify IDs from `configuratorDataUpdated` event |
| Wrong slot ID              | Material rejected | Verify slot IDs from configurator data          |

**Correct Pattern:**

```typescript
integrateMetabox('configurator-id', 'container', (api) => {
  // ✅ Correct: Commands sent after API ready
  api.sendCommandToMetabox(new SetProduct('product-id'));
  api.sendCommandToMetabox(new SetProductMaterial('slot-id', 'material-id'));
});

// ❌ Wrong: Command sent too early
// api.sendCommandToMetabox(new SetProduct('product-id'));
```

### Getting Help

If you're still experiencing issues:

1. Check the browser console for error messages
2. Verify your configurator ID in a separate browser tab
3. Test with a minimal example to isolate the issue
4. Contact support with: browser version, configurator URL, console errors, and steps to reproduce

## Tips & Tricks

- **Debounce rapid commands.** Avoid sending too many commands in quick succession — debounce material or product changes (e.g., 300 ms).
- **Responsive container.** Set the configurator container to responsive dimensions (`width: 100%; height: 100%;`) and adjust with media queries for mobile.
- **Events work in all modes.** `configuratorDataUpdated`, `screenshotReady`, and other events fire in both default and standalone mode — use them to keep your UI synchronized.
- **Wait for API ready.** Only send commands inside the `apiReadyCallback`. Commands sent before initialization are silently ignored.
- **Validate IDs from events.** Use the `configuratorDataUpdated` event payload to discover valid product, material, and slot IDs rather than hardcoding them.
- **HTTPS is required.** The iframe URL enforces HTTPS. HTTP URLs will be rejected. Use `localhost` for local development.
- **Pin CDN versions in production.** Replace `@latest` with a specific version to avoid unexpected breaking changes.

---

For more examples and advanced usage, visit
our [documentation site](https://preview.3dsource.com/front-libraries/master/basic-configurator-api-doc/).
