---
title: Authoring API
description: The public React and TypeScript surface for Odori videos, layouts, inputs, preparation, and assets.
---

## `defineVideoMetadata()`

Declares discovery-time metadata separately from the React component. The id
defaults to the entry's directory path under `videos/` and is validated when the
module loads. Pass one only to override the path.

```ts
defineVideoMetadata({
  id: "launch", // optional: defaults to the directory path
  title: "Product launch",
  description: "A concise introduction.",
  duration: "12s",
  layout: productLayout,
  schema: launchInput,
  defaultProps: {headline: "Ship the story."},
  tags: ["product", "launch"],
  thumbnailFrame: 45,
});
```

## `<Video>`

Provides the resolved layout, input, prepared data, asset registry, and
timeline context. It adds no duration of its own. Scene children are laid out
in order; any other child renders for the whole video.

## `<Scene>`

```tsx
<Scene id="demo" duration="8s" name="Product demo">
  <ProductDemo />
</Scene>
```

Each scene receives a local frame clock starting at zero and is mounted only
while it is on screen. Durations accept seconds (`8`), `"8s"`, `"500ms"`, or
`"45f"`.

## `<Stagger>`

An offset window inside a scene, for staggered layers.

```tsx
<Stagger from="1s" duration="2s">
  <Caption text="Preview needs no encode." />
</Stagger>
```

## `<Audio>`

Places a sound on the timeline. Cues declared inside a scene are offset by that
scene's start.

```tsx
<Audio src="/audio/bed.m4a" gain={0.75} fadeIn="1s" fadeOut="1.5s" duckUnder />
```

See [Audio](/docs/guides/audio) for placement, ducking, preview, and mixing.

## `<Fill>` and `<SafeArea>`

Layout primitives. `SafeArea` insets its children by the inherited layout safe
area.

## `defineVideoLayout()` and `defineBrand()`

Define inherited format and presentation policy. Layouts cannot perform async
work or add frames. See [Layouts and brands](/docs/guides/layouts).

## `defineInputSchema()`

A serializable input contract with `parse()`, `safeParse()`, `defaults()`, and
`describe()`. Studio generates controls from the description. Any zod-compatible
schema also works.

## `definePrepare()`

Defines the sole asynchronous data boundary. Its result must be serializable
and is frozen into the render manifest.

## Frame hooks

```tsx
const frame = useFrame();
const {fps, width, height, durationInFrames} = useVideo();
const scale = useDesignScale();
const brand = useBrand();
const layout = useLayout();
const scene = useScene();
const assets = useAssets();
```

Motion must derive from the current frame. Wall-clock timers, CSS keyframes,
and `requestAnimationFrame` are not deterministic render primitives.

## Motion helpers

```tsx
interpolate(frame, [0, 20], [0, 1], {easing: Easing.standard});
spring({frame, fps, from: 0.96, to: 1, damping: 20});
```

`interpolate` clamps by default, blends matching numeric segments inside
strings, and accepts `extrapolateLeft` and `extrapolateRight`. `spring` solves a
damped spring from the frame index alone.

## Playback

```tsx
const playback = usePlayback({fps: 30, durationInFrames: 360, autoPlay: true});
playback.toggle();
playback.step(1);
playback.seek(120);
```

`<Player>` wraps this hook with a canvas and controls. `usePlayback` is exported
so a custom surface, such as Studio, can own its own transport.

## Rendering surfaces

| Export | Purpose |
| --- | --- |
| `OdoriRuntime` | Mount one video at one frame |
| `Player` | Seekable player with optional controls |
| `RenderSurface` | The render worker target, with a frame setter and readiness handshake |
| `createRenderManifest` | Freeze inputs, format, scenes, audio, assets, and fonts |
| `useAudioPlayback` | Drive cue playback from a frame clock |
