# @hiyve/react-clips

Clip composition tool with client-side recording, multi-clip grid, and media playback for Hiyve video conferencing.

## Installation

```bash
npm install @hiyve/react-clips
```

**Peer dependencies:**

```bash
npm install @hiyve/react @hiyve/utilities @mui/material @mui/icons-material @emotion/react @emotion/styled react
```

**Optional peer dependencies:**

- `@hiyve/react-collaboration` — enables session management (`ClipsSession` component)
- `@hiyve/react-media-player` — enhanced clip playback with waveforms, regions, and volume controls (falls back to native `<audio>` / `<video>` elements)

## Quick Start

```tsx
import { HiyveProvider } from '@hiyve/react';
import { ClipComposition } from '@hiyve/react-clips';

function App() {
  return (
    <HiyveProvider apiKey="your-api-key" roomName="my-room">
      <ClipComposition
        enableRecording
        enableDragReorder
        maxClips={10}
        onClipAdded={(clip) => console.log('Added:', clip.name)}
        onError={(err) => console.error(err)}
      />
    </HiyveProvider>
  );
}
```

## Components

| Component | Description |
|-----------|-------------|
| `ClipComposition` | Main composition component with recording, multi-clip grid, and auto-save |
| `ClipsSession` | Session wrapper with file management (requires `@hiyve/react-collaboration`) |
| `CreateCompositionDialog` | MUI dialog for naming a new composition before creation |
| `ClipRecorder` | Recording panel for capturing audio and video clips |
| `ClipPlayer` | Single clip playback (wraps `@hiyve/react-media-player` or falls back to native elements) |
| `ClipGrid` | Responsive grid layout with drag-and-drop reorder |
| `ClipToolbar` | Composition controls (lock, play all, collaboration mode, recorder toggle) |

## Hooks

| Hook | Description |
|------|-------------|
| `useClipRecorder` | Client-side recording via the MediaRecorder API. Manages device selection, recording lifecycle, and clip upload. |
| `useClipPersistence` | Auto-save composition data with configurable interval, manual save, and unsaved change tracking. |

## Utilities

| Function | Description |
|----------|-------------|
| `generateCompositionId()` | Generate a unique composition ID |
| `generateClipId()` | Generate a unique clip ID |
| `getSupportedMimeType(mediaType)` | Get the first browser-supported MIME type for recording |
| `formatRecordingDuration(seconds)` | Format seconds as `mm:ss` string |
| `getFileExtensionFromMime(mimeType)` | Get file extension (`.webm`, `.ogg`, `.mp4`) from a MIME type |
| `createCompositionFile(client, options)` | Create and upload a new composition JSON file |

## ClipComposition Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `initialComposition` | `ClipCompositionFile` | — | Initial composition data to load |
| `title` | `string` | — | Composition title |
| `fileId` | `string` | — | Existing file ID for updates |
| `fileLocation` | `string` | `'/Clips'` | Storage location for composition files |
| `enableAutoSave` | `boolean` | `true` | Auto-save composition changes |
| `enableRecording` | `boolean` | `true` | Show recording controls |
| `enableRegions` | `boolean` | `false` | Enable named regions on clip players |
| `enableAudioPassthrough` | `boolean` | `false` | Enable audio passthrough mode |
| `enableDragReorder` | `boolean` | `true` | Enable drag-and-drop clip reordering |
| `maxClips` | `number` | `20` | Maximum number of clips in the composition |
| `maxRecordingDuration` | `number` | `300` | Maximum recording duration in seconds |
| `autoSaveInterval` | `number` | `5000` | Auto-save interval in milliseconds |
| `showHeader` | `boolean` | `true` | Show header with title and save status |
| `showToolbar` | `boolean` | `true` | Show toolbar with controls |
| `readOnly` | `boolean` | `false` | Disable editing |
| `labels` | `Partial<ClipCompositionLabels>` | — | Custom text labels |
| `icons` | `Partial<ClipCompositionIcons>` | — | Custom icons (ReactNode) |
| `colors` | `Partial<ClipCompositionColors>` | — | Custom color values |
| `styles` | `Partial<ClipCompositionStyles>` | — | Custom style values |
| `onAutoSave` | `(fileId: string) => void` | — | Called after auto-save completes |
| `onSaveError` | `(error: Error) => void` | — | Called when a save error occurs |
| `onChange` | `(composition: ClipCompositionFile) => void` | — | Called when the composition changes |
| `onClipAdded` | `(clip: ClipItem) => void` | — | Called when a clip is added |
| `onClipRemoved` | `(clipId: string) => void` | — | Called when a clip is removed |
| `onError` | `(error: Error) => void` | — | General error callback |
| `maxHeight` | `string \| number` | — | Maximum container height |
| `sx` | `SxProps<Theme>` | — | MUI sx styling prop |

## Imperative Handle

`ClipComposition` exposes a ref with the following methods:

```tsx
import { useRef } from 'react';
import { ClipComposition, type ClipCompositionRef } from '@hiyve/react-clips';

function MyEditor() {
  const ref = useRef<ClipCompositionRef>(null);

  return (
    <>
      <ClipComposition ref={ref} />
      <button onClick={() => ref.current?.save()}>Save Now</button>
    </>
  );
}
```

| Method / Property | Type | Description |
|-------------------|------|-------------|
| `save()` | `() => Promise<string \| null>` | Trigger a manual save, returns file ID |
| `isSaving` | `boolean` | Whether currently saving |
| `hasUnsavedChanges` | `boolean` | Whether there are unsaved changes |
| `lastSaved` | `Date \| null` | Timestamp of the last save |
| `fileId` | `string \| null` | Current file ID |
| `addClip(clip)` | `(clip: ClipItem) => void` | Add a clip programmatically |
| `removeClip(clipId)` | `(clipId: string) => void` | Remove a clip by ID |
| `getComposition()` | `() => ClipCompositionFile` | Get the current composition data |

## Using Hooks Directly

### useClipRecorder

Record audio or video clips with device selection and max duration enforcement:

```tsx
import { useClipRecorder } from '@hiyve/react-clips';

function MyRecorder() {
  const recorder = useClipRecorder({
    maxDuration: 120, // 2 minutes
    onError: (err) => console.error(err),
  });

  return (
    <div>
      <p>State: {recorder.state}</p>
      {recorder.state === 'idle' && (
        <button onClick={recorder.initialize}>Initialize</button>
      )}
      {recorder.state === 'ready' && (
        <button onClick={recorder.start}>Record</button>
      )}
      {recorder.state === 'recording' && (
        <>
          <span>{recorder.duration}s</span>
          <button onClick={recorder.pause}>Pause</button>
          <button onClick={recorder.stop}>Stop</button>
        </>
      )}
      {recorder.state === 'paused' && (
        <>
          <button onClick={recorder.resume}>Resume</button>
          <button onClick={recorder.stop}>Stop</button>
        </>
      )}
      {recorder.state === 'stopped' && recorder.previewUrl && (
        <>
          <audio src={recorder.previewUrl} controls />
          <button onClick={recorder.discard}>Discard</button>
        </>
      )}
    </div>
  );
}
```

**Recorder state machine:** `idle` → `requesting` → `ready` → `recording` ⇄ `paused` → `stopped` → `saving` → `ready`

### useClipPersistence

Auto-save composition data with retry logic:

```tsx
import { useClipPersistence } from '@hiyve/react-clips';

const persistence = useClipPersistence({
  client,
  clips,
  title: 'My Composition',
  lockMode: 'unlocked',
  collaborationMode: 'public',
  enabled: true,
  userId: 'user-123',
  autoSaveInterval: 5000,
  onSaved: (fileId) => console.log('Saved:', fileId),
  onError: (err) => console.error(err),
});

// persistence.save()            — trigger manual save
// persistence.isSaving          — currently saving?
// persistence.hasUnsavedChanges — pending changes?
// persistence.lastSaved         — last save timestamp
// persistence.fileId            — current file ID
// persistence.markUnsaved()     — mark as changed
```

## Session Management

`ClipsSession` integrates with `@hiyve/react-collaboration` for file management:

```tsx
import { ClipsSession } from '@hiyve/react-clips';

<ClipsSession
  labels={{
    emptyState: { title: 'My Clips', createButton: 'New' },
  }}
  clipCompositionProps={{
    enableRecording: true,
    maxClips: 10,
  }}
  onError={(err) => console.error(err)}
  sx={{ height: '100%' }}
/>
```

The session provides an empty state with a file list and create button. Clicking "New Composition" opens a `CreateCompositionDialog` to name the composition before creation. When a composition is opened or created, it renders `ClipComposition` with a session header for save/close controls.

## Customization

All visual aspects are customizable through four prop objects. Only override the keys you want to change — defaults are applied for the rest.

### Labels (ClipCompositionLabels)

~40 text strings covering header, save status, toolbar, recorder, player, grid, collaboration, and error messages. Includes two functions: `lastSaved(date)` and `clipCount(count)`.

```tsx
<ClipComposition
  labels={{
    title: 'Sound Clips',
    startRecording: 'Record',
    noClips: 'No clips yet — start recording!',
    lastSaved: (date) => `Saved at ${date.toLocaleTimeString()}`,
  }}
/>
```

### Icons (ClipCompositionIcons)

19 icon slots, each accepting a `ReactNode`. Defaults use `@mui/icons-material`.

```tsx
import { MicNone, FiberManualRecord } from '@mui/icons-material';

<ClipComposition
  icons={{
    recordAudio: <MicNone />,
    startRecording: <FiberManualRecord color="error" />,
  }}
/>
```

### Colors (ClipCompositionColors)

24 color values for container, header, toolbar, recorder, grid, clip cards, save status, and collaboration mode indicators.

```tsx
<ClipComposition
  colors={{
    containerBackground: '#2d2d2d',
    recordingIndicator: '#e53935',
    clipCardBackground: '#383838',
  }}
/>
```

### Styles (ClipCompositionStyles)

12 numeric/string values for border radius, padding, grid layout, and component dimensions.

```tsx
<ClipComposition
  styles={{
    borderRadius: 12,
    gridGap: 16,
    gridMinColumnWidth: 320,
    playerHeight: 150,
  }}
/>
```

## Defaults and Merge Functions

| Default Object | Merge Function |
|----------------|----------------|
| `defaultClipCompositionLabels` | `mergeClipCompositionLabels(overrides?)` |
| `defaultClipCompositionIcons` | `mergeClipCompositionIcons(overrides?)` |
| `defaultClipCompositionColors` | `mergeClipCompositionColors(overrides?)` |
| `defaultClipCompositionStyles` | `mergeClipCompositionStyles(overrides?)` |
| `defaultClipsSessionLabels` | `mergeClipsSessionLabels(overrides?)` |
| `defaultCreateCompositionDialogLabels` | `mergeCreateCompositionDialogLabels(overrides?)` |

## Features

- Record audio and video clips directly in the browser
- Multi-clip responsive grid with drag-and-drop reordering
- Enhanced playback with waveforms, named regions, and volume controls (when `@hiyve/react-media-player` is installed)
- Native `<audio>` / `<video>` fallback when media player is not available
- Auto-save with configurable interval and manual save trigger
- Lock/unlock compositions to prevent editing
- Private/public collaboration modes for clip visibility
- Device selection for microphone and camera
- Pause/resume recording with duration tracking
- Max recording duration enforcement (configurable, hard limit 30 minutes)
- Imperative ref API for programmatic control
- Session management with file list integration (when `@hiyve/react-collaboration` is installed)
- Works both inside and outside `HiyveProvider`
- Fully customizable labels, icons, colors, and styles

## Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `DEFAULT_FILE_LOCATION` | `'/Clips'` | Default storage location |
| `CLIP_COMPOSITION_FILE_EXTENSION` | `'.json'` | Composition file extension |
| `CLIP_COMPOSITION_FILE_MIME_TYPE` | `'application/json'` | Composition MIME type |
| `CLIP_RESOURCE_TYPE` | `'clip-composition'` | Resource type identifier for composition files |
| `CLIP_VIDEO_RESOURCE_TYPE` | `'clip'` | Resource type identifier for individual video clip files |
| `CLIP_AUDIO_RESOURCE_TYPE` | `'clip-audio'` | Resource type identifier for individual audio clip files |
| `DEFAULT_AUTO_SAVE_INTERVAL` | `5000` | Auto-save interval (ms) |
| `DEFAULT_MAX_RECORDING_DURATION` | `300` | Default max recording (seconds) |
| `MAX_RECORDING_DURATION_HARD` | `1800` | Hard limit on recording (seconds) |
| `RECORDING_TIMESLICE` | `1000` | MediaRecorder timeslice (ms) |
| `DEFAULT_MAX_CLIPS` | `20` | Default max clips per composition |
| `DEFAULT_GRID_COLUMNS` | `2` | Default grid columns |
| `PREFERRED_AUDIO_MIME_TYPES` | `string[]` | Audio MIME type preference order |
| `PREFERRED_VIDEO_MIME_TYPES` | `string[]` | Video MIME type preference order |
| `COLLABORATION_MODES` | `['private', 'public']` | Available collaboration modes |
| `LOCK_MODES` | `['locked', 'unlocked']` | Available lock modes |

## Key Types

| Type | Description |
|------|-------------|
| `ClipItem` | A single clip with media type, file reference, duration, regions, and metadata |
| `ClipCompositionFile` | Full composition data: title, clips array, lock/collaboration modes, author info |
| `ClipMediaType` | `'audio' \| 'video'` |
| `CompositionLockMode` | `'locked' \| 'unlocked'` |
| `CollaborationMode` | `'private' \| 'public'` |
| `RecorderState` | `'idle' \| 'requesting' \| 'ready' \| 'recording' \| 'paused' \| 'stopped' \| 'saving'` |
| `RegionData` | Named region for clip playback (id, start, end, content) |
| `GridLayoutItem` | Custom grid position for a clip |
| `ClipFileClient` | File client interface for upload and URL resolution |
| `ClipCompositionRef` | Imperative handle exposed by `ClipComposition` |
| `CreateCompositionDialogLabels` | Customizable labels for the create composition dialog |
| `CreateCompositionDialogProps` | Props for `CreateCompositionDialog` |

## Style Utilities

Seven style-generating functions for building custom layouts:

| Function | Description |
|----------|-------------|
| `getContainerStyles(colors, styles)` | Main container styles |
| `getHeaderStyles(colors, styles)` | Header bar styles |
| `getToolbarStyles(colors, styles)` | Toolbar styles |
| `getRecorderStyles(colors, styles)` | Recorder panel styles |
| `getGridStyles(colors, styles)` | CSS Grid layout styles |
| `getClipCardStyles(colors, styles, isDragging?)` | Individual clip card styles |
| `getEmptyStateStyles(colors)` | Empty state placeholder styles |

## Requirements

- **React 18+**
- **MUI v5 or v6** (`@mui/material`, `@mui/icons-material`, `@emotion/react`, `@emotion/styled`)
- **`@hiyve/react`** — provides `HiyveProvider` context (components work outside the provider with limited functionality)
- **`@hiyve/utilities`** — debug logging and retry utilities
- **Browser support**: MediaRecorder API required for recording (Chrome, Firefox, Edge, Safari 14.1+)
