---
title: "Multi Surface Render"
description: "Multi-surface rendering with json-render — same JSON spec produces React web, Next.js apps, React Native, Ink terminal UIs, PDFs, emails, Remotion videos, OG images, and 3D scenes. Covers renderer target selection, registry mapping, and platform-specific APIs (renderToBuffer, renderToStream, renderToFile). Use when generating output for multiple platforms, creating PDF reports, email templates, demo videos, or social media images from a single component spec."
---

<span className="badge badge-gray">Reference</span> <span className="badge badge-yellow">medium</span>

> **Auto-activated** — this skill loads automatically when Claude detects matching context.

<ContextualSkillSidebar slug="multi-surface-render" />


# Multi-Surface Rendering with json-render

Define once, render everywhere. A single json-render catalog and spec can produce React web UIs, PDF reports, HTML emails, Remotion demo videos, and OG images — each surface gets its own registry that maps catalog types to platform-native components.

## Quick Reference

| Category | Rules | Impact | When to Use |
|----------|-------|--------|-------------|
| [Target Selection](#target-selection) | 1 | HIGH | Choosing which renderer for your use case |
| [React Renderer](#react-renderer) | 1 | MEDIUM | Web apps, SPAs, dashboards |
| [PDF & Email Renderer](#pdf--email-renderer) | 1 | HIGH | Reports, documents, notifications |
| [Video & Image Renderer](#video--image-renderer) | 1 | MEDIUM | Demo videos, OG images, social cards |
| [Registry Mapping](#registry-mapping) | 1 | HIGH | Platform-specific component implementations |

**Total: 5 rules across 5 categories**

## How Multi-Surface Rendering Works

1. **One catalog** — Zod-typed component definitions shared across all surfaces
2. **One spec** — flat-tree JSON/YAML describing the UI structure
3. **Many registries** — each surface maps catalog types to its own component implementations
4. **Many renderers** — each package renders the spec using its registry

The catalog is the contract. The spec is the data. The registry is the platform-specific implementation.

## Quick Start — Same Catalog, Different Renderers

### Shared Catalog (used by all surfaces)

```typescript
import { defineCatalog } from '@json-render/core'
import { z } from 'zod'

export const catalog = defineCatalog({
  Heading: {
    props: z.object({
      text: z.string(),
      level: z.enum(['h1', 'h2', 'h3']),
    }),
    children: false,
  },
  Paragraph: {
    props: z.object({ text: z.string() }),
    children: false,
  },
  StatCard: {
    props: z.object({
      label: z.string(),
      value: z.string(),
      trend: z.enum(['up', 'down', 'flat']).optional(),
    }),
    children: false,
  },
})
```

### Render to Web (React)

```tsx
import { Renderer } from '@json-render/react'
import { catalog } from './catalog'
import { webRegistry } from './registries/web'

export const Dashboard = ({ spec }) => (
  <Renderer spec={spec} catalog={catalog} registry={webRegistry} />
)
```

### Render to PDF

```typescript
import { renderToBuffer, renderToFile } from '@json-render/react-pdf'
import { catalog } from './catalog'
import { pdfRegistry } from './registries/pdf'

// Buffer for HTTP response
const buffer = await renderToBuffer(spec, { catalog, registry: pdfRegistry })

// Direct file output
await renderToFile(spec, './output/report.pdf', { catalog, registry: pdfRegistry })
```

### Render to Email

```typescript
import { renderToHtml } from '@json-render/react-email'
import { catalog } from './catalog'
import { emailRegistry } from './registries/email'

const html = await renderToHtml(spec, { catalog, registry: emailRegistry })
await sendEmail({ to: user.email, subject: 'Weekly Report', html })
```

### Render to OG Image (Satori)

```typescript
import { renderToSvg, renderToPng } from '@json-render/image'
import { catalog } from './catalog'
import { imageRegistry } from './registries/image'

const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})
```

### Render to Video (Remotion)

```tsx
import { JsonRenderComposition } from '@json-render/remotion'
import { catalog } from './catalog'
import { remotionRegistry } from './registries/remotion'

export const DemoVideo = () => (
  <JsonRenderComposition
    spec={spec}
    catalog={catalog}
    registry={remotionRegistry}
    fps={30}
    durationInFrames={150}
  />
)
```

### Render to Terminal (Ink, 0.15+)

```tsx
import { render } from 'ink'
import { InkRenderer } from '@json-render/ink'
import { catalog } from './catalog'
import { inkRegistry } from './registries/ink'

render(<InkRenderer spec={spec} catalog={catalog} registry={inkRegistry} />)
```

Useful for `/ork:*` CLI dashboards and streaming agent chat interfaces — ships 20+ Ink-native components (Box, Text, Spinner, Table, Markdown, Progress, etc.).

### Render to Next.js App (0.16+)

```typescript
import { generateNextApp } from '@json-render/next'

await generateNextApp(spec, {
  catalog,
  registry: webRegistry,
  outDir: './out',
  // generates routes, layouts, SSR handlers, and metadata
})
```

Output is a full Next.js App Router project — specs describe route trees, not just components.

## Decision Matrix — When to Use Each Target

| Target | Package | When to Use | Output |
|--------|---------|-------------|--------|
| React | `@json-render/react` | Web apps, SPAs | JSX |
| Next.js | `@json-render/next` *(0.16+)* | Full apps: routes, layouts, SSR, metadata | Next.js app |
| Vue | `@json-render/vue` | Vue projects | Vue components |
| Svelte | `@json-render/svelte` | Svelte projects | Svelte components |
| Svelte+shadcn | `@json-render/shadcn-svelte` *(0.16+)* | 36-component Svelte 5 catalog | Svelte + Tailwind |
| React Native | `@json-render/react-native` | Mobile apps (25+ components) | Native views |
| Terminal | `@json-render/ink` *(0.15+)* | CLI UIs, TUIs, streaming chat | Ink (terminal) |
| PDF | `@json-render/react-pdf` | Reports, documents | PDF buffer/file |
| Email | `@json-render/react-email` | Notifications, digests | HTML string |
| Remotion | `@json-render/remotion` | Demo videos, marketing | MP4/WebM |
| Image | `@json-render/image` | OG images, social cards | SVG/PNG (Satori) |
| YAML | `@json-render/yaml` *(0.14+)* | Token optimization, streaming parser | YAML string |
| MCP | `@json-render/mcp` | Claude/Cursor/ChatGPT conversations | Sandboxed iframe |
| 3D | `@json-render/react-three-fiber` | 3D scenes (20 components, incl. `GaussianSplat` in 0.17) | Three.js canvas |
| Codegen | `@json-render/codegen` | Source code from specs | TypeScript/JSX |

Load `rules/target-selection.md` for detailed selection criteria and trade-offs.

## PDF Renderer — Reports and Documents

The `@json-render/react-pdf` package renders specs to PDF using react-pdf under the hood. Three output modes: buffer, file, and stream.

```typescript
import { renderToBuffer, renderToFile, renderToStream } from '@json-render/react-pdf'

// In-memory buffer (for HTTP responses, S3 upload)
const buffer = await renderToBuffer(spec, { catalog, registry: pdfRegistry })
res.setHeader('Content-Type', 'application/pdf')
res.send(buffer)

// Direct file write
await renderToFile(spec, './output/report.pdf', { catalog, registry: pdfRegistry })

// Streaming (for large documents)
const stream = await renderToStream(spec, { catalog, registry: pdfRegistry })
stream.pipe(res)
```

Load `rules/pdf-email-renderer.md` for PDF registry patterns and email rendering.

## Image Renderer — OG Images and Social Cards

The `@json-render/image` package uses Satori to convert specs to SVG, then optionally to PNG. Designed for server-side generation of social media images.

```typescript
import { renderToSvg, renderToPng } from '@json-render/image'

// SVG output (smaller, scalable)
const svg = await renderToSvg(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

// PNG output (universal compatibility)
const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})
```

Load `rules/video-image-renderer.md` for Satori constraints and Remotion composition patterns.

## Registry Mapping — Same Catalog, Platform-Specific Components

Each surface needs its own registry. The registry maps catalog types to platform-specific component implementations while the catalog and spec stay identical.

```typescript
// Web registry — uses HTML elements
const webRegistry = {
  Heading: ({ text, level }) => {
    const Tag = level // h1, h2, h3
    return <Tag className="font-bold">{text}</Tag>
  },
  StatCard: ({ label, value, trend }) => (
    <div className="rounded border p-4">
      <span className="text-sm text-gray-500">{label}</span>
      <strong className="text-2xl">{value}</strong>
    </div>
  ),
}

// PDF registry — uses react-pdf primitives
import { Text, View } from '@react-pdf/renderer'
const pdfRegistry = {
  Heading: ({ text, level }) => (
    <Text style={{ fontSize: level === 'h1' ? 24 : level === 'h2' ? 18 : 14 }}>
      {text}
    </Text>
  ),
  StatCard: ({ label, value }) => (
    <View style={{ border: '1pt solid #ccc', padding: 8 }}>
      <Text style={{ fontSize: 10, color: '#666' }}>{label}</Text>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>{value}</Text>
    </View>
  ),
}
```

Load `rules/registry-mapping.md` for registry creation patterns and type safety.

## Rule Details

### Target Selection

Decision criteria for choosing the right renderer target.

| Rule | File | Key Pattern |
|------|------|-------------|
| Target Selection | `rules/target-selection.md` | Use case mapping, output format constraints |

### React Renderer

Web rendering with the `&lt;Renderer&gt;` component.

| Rule | File | Key Pattern |
|------|------|-------------|
| React Renderer | `rules/react-renderer.md` | `&lt;Renderer&gt;` component, streaming, error boundaries |

### PDF & Email Renderer

Server-side rendering to PDF buffers/files and HTML email strings.

| Rule | File | Key Pattern |
|------|------|-------------|
| PDF & Email | `rules/pdf-email-renderer.md` | renderToBuffer, renderToFile, renderToHtml |

### Video & Image Renderer

Remotion compositions and Satori image generation.

| Rule | File | Key Pattern |
|------|------|-------------|
| Video & Image | `rules/video-image-renderer.md` | JsonRenderComposition, renderToPng, renderToSvg |

### Registry Mapping

Creating platform-specific registries for a shared catalog.

| Rule | File | Key Pattern |
|------|------|-------------|
| Registry Mapping | `rules/registry-mapping.md` | Per-platform registries, type-safe mapping |

## Key Decisions

| Decision | Recommendation |
|----------|----------------|
| PDF library | Use `@json-render/react-pdf` (react-pdf), not Puppeteer screenshots |
| Email rendering | Use `@json-render/react-email` (react-email), not MJML or custom HTML |
| OG images | Use `@json-render/image` (Satori), not Puppeteer or canvas |
| Video | Use `@json-render/remotion` (Remotion), not FFmpeg scripts |
| Registry per platform | Always separate registries; never one registry for all surfaces |
| Catalog sharing | One catalog definition shared via import across all registries |

## Common Mistakes

1. Building separate component trees for each surface — defeats the purpose; share the catalog and spec
2. Using Puppeteer to screenshot React for PDF generation — slow, fragile; use native react-pdf rendering
3. One giant registry covering all platforms — impossible since PDF uses `&lt;View&gt;`/`&lt;Text&gt;`, web uses `<div>`/`<span>`
4. Forgetting Satori limitations — no CSS grid, limited flexbox; design image registries with these constraints
5. Duplicating catalog definitions per surface — one catalog, many registries; the catalog is the contract

## Related Skills

- `ork:json-render-catalog` — Catalog definition patterns with Zod, shadcn components
- `ork:demo-producer` — Video production pipeline using Remotion
- `ork:presentation-builder` — Slide deck generation
- `ork:mcp-visual-output` — Rendering specs in Claude/Cursor via MCP


---

## Rules (5)

### Use native react-pdf and react-email renderers instead of browser-based workarounds — HIGH


## PDF & Email Renderer

`@json-render/react-pdf` renders specs to PDF using react-pdf primitives (`View`, `Text`, `Image`). `@json-render/react-email` renders specs to HTML email strings using react-email components. Both validate against the same catalog.

**Incorrect — using Puppeteer to screenshot React for PDF:**
```typescript
// WRONG: Launches a browser, takes a screenshot, converts to PDF
import puppeteer from 'puppeteer'

async function generatePdf(spec) {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.setContent(renderToString(<Dashboard spec={spec} />))
  const pdf = await page.pdf({ format: 'A4' })
  await browser.close()
  return pdf // slow, ~2s startup, CSS rendering differences, no catalog validation
}
```

**Correct — native PDF rendering via react-pdf:**
```typescript
import { renderToBuffer, renderToFile, renderToStream } from '@json-render/react-pdf'
import { catalog } from './catalog'
import { pdfRegistry } from './registries/pdf'

// Buffer — for HTTP responses, S3 upload, attachments
const buffer = await renderToBuffer(spec, { catalog, registry: pdfRegistry })

// File — direct disk write
await renderToFile(spec, './output/report.pdf', { catalog, registry: pdfRegistry })

// Stream — for large documents, pipe to HTTP response
const stream = await renderToStream(spec, { catalog, registry: pdfRegistry })
res.setHeader('Content-Type', 'application/pdf')
stream.pipe(res)
```

**Incorrect — manual HTML string for email:**
```typescript
// WRONG: Manual HTML, no validation, rendering inconsistencies
const html = `<table><tr><td>${data.title}</td></tr></table>`
```

**Correct — react-email rendering:**
```typescript
import { renderToHtml } from '@json-render/react-email'
import { catalog } from './catalog'
import { emailRegistry } from './registries/email'

const html = await renderToHtml(spec, { catalog, registry: emailRegistry })
await transporter.sendMail({ to: user.email, subject: 'Report', html })
```

**Key rules:**
- Use `renderToBuffer` for in-memory PDF (HTTP responses, email attachments, cloud storage upload)
- Use `renderToFile` for disk output (batch report generation, CI artifacts)
- Use `renderToStream` for large documents to avoid buffering the entire PDF in memory
- PDF registry components must use react-pdf primitives (`View`, `Text`, `Image`) — not HTML elements
- Email registry components must use react-email primitives (`Section`, `Text`, `Heading`) — not arbitrary HTML
- Both renderers validate specs against the catalog — invalid types or props throw at render time

### PDF Registry Pattern

```typescript
import { View, Text, StyleSheet } from '@react-pdf/renderer'

const styles = StyleSheet.create({
  heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  card: { border: '1pt solid #e5e7eb', padding: 12, borderRadius: 4 },
})

export const pdfRegistry = {
  Heading: ({ text, level }) => (
    <Text style={{ ...styles.heading, fontSize: level === 'h1' ? 24 : level === 'h2' ? 18 : 14 }}>
      {text}
    </Text>
  ),
  StatCard: ({ label, value }) => (
    <View style={styles.card}>
      <Text style={{ fontSize: 10, color: '#6b7280' }}>{label}</Text>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>{value}</Text>
    </View>
  ),
}
```


### Use the Renderer component with catalog validation for web rendering — MEDIUM


## React Renderer

The `&lt;Renderer&gt;` component from `@json-render/react` validates specs against the catalog at runtime and renders each element using the registry. Never parse specs manually.

**Incorrect — parsing the spec manually:**
```tsx
// WRONG: Manual parsing, no validation, no streaming
function Dashboard({ spec }) {
  return (
    <div>
      {Object.entries(spec.elements).map(([id, el]) => {
        const Component = components[el.type] // no catalog validation
        return <Component key={id} {...el.props} />
      })}
    </div>
  )
}
```

**Correct — using the Renderer component:**
```tsx
import { Renderer } from '@json-render/react'
import { catalog } from './catalog'
import { webRegistry } from './registries/web'

function Dashboard({ spec }) {
  return (
    <Renderer
      spec={spec}
      catalog={catalog}
      registry={webRegistry}
      fallback={<LoadingSkeleton />}
      onError={(err) => console.error('Render error:', err)}
    />
  )
}
```

**Key rules:**
- Always pass `catalog` to `&lt;Renderer&gt;` — it validates that spec types exist in the catalog and props match Zod schemas
- Use `fallback` prop for loading states during progressive streaming
- Use `onError` callback or wrap in an error boundary for graceful degradation
- For streaming specs (AI generating in real-time), the Renderer updates progressively as elements arrive
- The registry maps catalog types to React components — keep it separate from the catalog definition

### Progressive Streaming Pattern

```tsx
import { Renderer, useStreamingSpec } from '@json-render/react'

function StreamingDashboard({ specStream }) {
  const spec = useStreamingSpec(specStream) // updates as patches arrive

  return (
    <Renderer
      spec={spec}
      catalog={catalog}
      registry={webRegistry}
      fallback={<Skeleton />}
    />
  )
}
```

Elements render as soon as their props are complete — the user sees the UI building in real-time.


### Create separate registries per platform sharing a single catalog — HIGH


## Registry Mapping

A registry maps each catalog type to a platform-specific component implementation. The catalog (Zod schemas) and spec (flat-tree data) stay identical across surfaces. Only the registry changes.

**Incorrect — one giant registry trying to cover all platforms:**
```typescript
// WRONG: Impossible — PDF needs View/Text, web needs div/span
const universalRegistry = {
  Heading: ({ text, level, platform }) => {
    if (platform === 'pdf') return <Text style={...}>{text}</Text>
    if (platform === 'email') return <Heading as={level}>{text}</Heading>
    return <h1>{text}</h1> // web fallback
  },
}
```

**Correct — separate registries per platform, same catalog:**
```typescript
import { catalog } from './catalog' // SHARED — one definition

// Web registry
export const webRegistry = {
  Heading: ({ text, level }) => {
    const Tag = level
    return <Tag className="font-bold tracking-tight">{text}</Tag>
  },
  StatCard: ({ label, value, trend }) => (
    <div className="rounded-lg border p-4 shadow-sm">
      <p className="text-sm text-muted-foreground">{label}</p>
      <p className="text-2xl font-bold">{value}</p>
      {trend && <TrendIcon direction={trend} />}
    </div>
  ),
}

// PDF registry
import { View, Text } from '@react-pdf/renderer'
export const pdfRegistry = {
  Heading: ({ text, level }) => (
    <Text style={{ fontSize: level === 'h1' ? 24 : 18, fontWeight: 'bold' }}>
      {text}
    </Text>
  ),
  StatCard: ({ label, value }) => (
    <View style={{ border: '1pt solid #ccc', padding: 8 }}>
      <Text style={{ fontSize: 10, color: '#666' }}>{label}</Text>
      <Text style={{ fontSize: 18 }}>{value}</Text>
    </View>
  ),
}

// Email registry
import { Section, Text as EmailText, Heading as EmailHeading } from '@react-email/components'
export const emailRegistry = {
  Heading: ({ text, level }) => (
    <EmailHeading as={level}>{text}</EmailHeading>
  ),
  StatCard: ({ label, value }) => (
    <Section style={{ border: '1px solid #e5e7eb', padding: '12px' }}>
      <EmailText style={{ fontSize: '12px', color: '#6b7280' }}>{label}</EmailText>
      <EmailText style={{ fontSize: '20px', fontWeight: 'bold' }}>{value}</EmailText>
    </Section>
  ),
}
```

**Key rules:**
- One catalog, many registries — the catalog defines WHAT can be rendered, registries define HOW
- Every catalog type must have an entry in each registry — missing entries throw at render time
- Registry components receive the same props defined in the catalog Zod schema
- Never add platform-specific props to the catalog — the catalog is platform-agnostic
- Organize registries in `./registries/web.ts`, `./registries/pdf.ts`, `./registries/email.ts`
- Use `CatalogComponents&lt;typeof catalog&gt;` type to ensure registries match the catalog

### Type-Safe Registry Pattern

```typescript
import type { CatalogComponents } from '@json-render/react'
import type { catalog } from './catalog'

// TypeScript ensures every catalog type is implemented
export const webRegistry: CatalogComponents<typeof catalog> = {
  Heading: ({ text, level }) => { /* ... */ },
  StatCard: ({ label, value, trend }) => { /* ... */ },
  // Missing type → TypeScript error
}
```

### File Organization

```
src/
  catalog.ts              # Shared catalog (Zod schemas)
  registries/
    web.ts                # React/HTML components
    pdf.ts                # react-pdf View/Text components
    email.ts              # react-email Section/Text components
    image.ts              # Satori-compatible inline-style components
    remotion.ts           # Remotion-animated components
```


### Select renderer target based on output format and platform constraints — HIGH


## Target Selection

Choose the renderer target based on what the output is, not what framework you use. Each target maps to a specific `@json-render/*` package with its own rendering pipeline.

**Incorrect — building separate templates for each surface:**
```typescript
// WRONG: Separate template systems, no shared catalog
const webDashboard = buildReactComponents(data)
const pdfReport = buildPdfWithPuppeteer(data)     // puppeteer screenshot
const emailDigest = buildMjmlEmail(data)           // separate MJML templates
const ogImage = buildCanvasImage(data)             // manual canvas drawing
```

**Correct — one catalog, one spec, multiple registries:**
```typescript
import { catalog } from './catalog'

// Same spec, different renderers
import { Renderer } from '@json-render/react'           // web
import { renderToBuffer } from '@json-render/react-pdf' // pdf
import { renderToHtml } from '@json-render/react-email' // email
import { renderToPng } from '@json-render/image'        // og image

// Each renderer uses the same catalog + spec, different registry
const webUi = <Renderer spec={spec} catalog={catalog} registry={webRegistry} />
const pdf = await renderToBuffer(spec, { catalog, registry: pdfRegistry })
const html = await renderToHtml(spec, { catalog, registry: emailRegistry })
const png = await renderToPng(spec, { catalog, registry: imageRegistry, width: 1200, height: 630 })
```

**Key rules:**
- Match target to output format: PDF document = `react-pdf`, HTML email = `react-email`, image = `image`
- Never use Puppeteer/Playwright to screenshot a React page for PDF — use native `@json-render/react-pdf`
- Never build custom MJML/HTML templates when `@json-render/react-email` exists
- If output is a file (PDF, PNG, MP4), use the server-side renderer — not the React `&lt;Renderer&gt;` component
- Multiple targets in one project is the normal case — that is the entire point of json-render

### Selection Checklist

| Need | Target | Package |
|------|--------|---------|
| Interactive web UI | React | `@json-render/react` |
| Downloadable document | PDF | `@json-render/react-pdf` |
| Transactional email | Email | `@json-render/react-email` |
| Social preview card | Image | `@json-render/image` |
| Marketing video | Video | `@json-render/remotion` |
| Mobile app screen | React Native | `@json-render/react-native` |
| AI conversation output | MCP | `@json-render/mcp` |
| Source code generation | Codegen | `@json-render/codegen` |


### Use Remotion compositions and Satori for video and image generation from specs — MEDIUM


## Video & Image Renderer

`@json-render/remotion` wraps specs into Remotion compositions for MP4/WebM video. `@json-render/image` uses Satori to render specs as SVG, then optionally converts to PNG for OG images and social cards.

**Incorrect — manually creating Remotion timelines:**
```tsx
// WRONG: Manual timeline, no catalog validation, duplicated rendering logic
export const MyVideo = () => (
  <Composition
    id="demo"
    component={() => (
      <div>
        <h1>{data.title}</h1>
        <p>{data.description}</p>
      </div>
    )}
    durationInFrames={150}
    fps={30}
    width={1920}
    height={1080}
  />
)
```

**Correct — JsonRenderComposition from spec:**
```tsx
import { JsonRenderComposition } from '@json-render/remotion'
import { catalog } from './catalog'
import { remotionRegistry } from './registries/remotion'

export const DemoVideo = () => (
  <JsonRenderComposition
    spec={spec}
    catalog={catalog}
    registry={remotionRegistry}
    fps={30}
    durationInFrames={150}
    width={1920}
    height={1080}
  />
)
```

**Incorrect — using Puppeteer for OG images:**
```typescript
// WRONG: Launches browser, screenshots a page, saves as PNG
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setViewport({ width: 1200, height: 630 })
await page.setContent(`<div style="...">${title}</div>`)
const png = await page.screenshot({ type: 'png' })
```

**Correct — Satori-based image rendering:**
```typescript
import { renderToSvg, renderToPng } from '@json-render/image'
import { catalog } from './catalog'
import { imageRegistry } from './registries/image'

// SVG (smaller file size, scalable)
const svg = await renderToSvg(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})

// PNG (universal compatibility)
const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})
```

**Key rules:**
- Use `JsonRenderComposition` for Remotion videos — it validates specs against the catalog
- Use `renderToPng` for OG images (1200x630 standard) — Satori is server-side, no browser needed
- Satori has CSS limitations: no CSS grid, limited flexbox, no `position: absolute` nesting — design image registries accordingly
- Image registries must use inline styles only — Satori does not support className or CSS files
- For Remotion, the registry can use Remotion animation primitives (`useCurrentFrame`, `interpolate`, `spring`)

### Satori CSS Constraints

| Supported | Not Supported |
|-----------|---------------|
| Flexbox (basic) | CSS Grid |
| `border`, `borderRadius` | `box-shadow` |
| `padding`, `margin` | `position: absolute` (limited) |
| `fontSize`, `fontWeight` | External CSS, className |
| `color`, `backgroundColor` | CSS animations |
| `width`, `height` | Media queries |

### Common OG Image Dimensions

| Platform | Width | Height | Ratio |
|----------|-------|--------|-------|
| Open Graph (general) | 1200 | 630 | 1.91:1 |
| Twitter card | 1200 | 628 | 1.91:1 |
| LinkedIn share | 1200 | 627 | 1.91:1 |
| Facebook share | 1200 | 630 | 1.91:1 |



---

## References (6)

### Renderer Api

# Renderer API Reference

## @json-render/react

### `&lt;Renderer&gt;` Component

```tsx
import { Renderer } from '@json-render/react'

<Renderer
  spec={spec}                    // JsonRenderSpec — flat-tree JSON/YAML
  catalog={catalog}              // Catalog from defineCatalog()
  registry={registry}            // CatalogComponents<typeof catalog>
  fallback={<Loading />}         // Optional: shown during streaming
  onError={(err) => log(err)}    // Optional: error callback
/>
```

### `useStreamingSpec` Hook

```tsx
import { useStreamingSpec } from '@json-render/react'

const spec = useStreamingSpec(stream) // ReadableStream of JSON Patch ops
```

---

## @json-render/react-pdf

### `renderToBuffer(spec, options): Promise&lt;Buffer&gt;`

Renders spec to an in-memory PDF buffer.

```typescript
import { renderToBuffer } from '@json-render/react-pdf'

const buffer = await renderToBuffer(spec, {
  catalog,
  registry: pdfRegistry,
  pageSize: 'A4',               // Optional: 'A4' | 'LETTER' | { width, height }
  orientation: 'portrait',      // Optional: 'portrait' | 'landscape'
  margins: { top: 40, bottom: 40, left: 40, right: 40 }, // Optional
})
```

### `renderToFile(spec, path, options): Promise&lt;void&gt;`

Renders spec directly to a PDF file on disk.

```typescript
import { renderToFile } from '@json-render/react-pdf'

await renderToFile(spec, './output/report.pdf', {
  catalog,
  registry: pdfRegistry,
  pageSize: 'A4',
})
```

### `renderToStream(spec, options): Promise&lt;ReadableStream&gt;`

Renders spec to a readable stream for piping to HTTP responses.

```typescript
import { renderToStream } from '@json-render/react-pdf'

const stream = await renderToStream(spec, { catalog, registry: pdfRegistry })
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"')
stream.pipe(res)
```

---

## @json-render/react-email

### `renderToHtml(spec, options): Promise&lt;string&gt;`

Renders spec to an HTML string optimized for email clients.

```typescript
import { renderToHtml } from '@json-render/react-email'

const html = await renderToHtml(spec, {
  catalog,
  registry: emailRegistry,
  preview: 'Your weekly report is ready', // Optional: email preview text
  theme: 'light',                          // Optional: 'light' | 'dark'
})
```

### `renderToPlainText(spec, options): Promise&lt;string&gt;`

Renders spec to plain text (for text/plain multipart emails).

```typescript
import { renderToPlainText } from '@json-render/react-email'

const text = await renderToPlainText(spec, { catalog, registry: emailRegistry })
```

---

## @json-render/image

### `renderToSvg(spec, options): Promise&lt;string&gt;`

Renders spec to an SVG string using Satori.

```typescript
import { renderToSvg } from '@json-render/image'

const svg = await renderToSvg(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,              // Required
  height: 630,              // Required
  fonts: [{                 // Optional: custom fonts
    name: 'Inter',
    data: fontBuffer,
    weight: 400,
    style: 'normal',
  }],
})
```

### `renderToPng(spec, options): Promise&lt;Buffer&gt;`

Renders spec to a PNG buffer (SVG -> PNG via Resvg).

```typescript
import { renderToPng } from '@json-render/image'

const png = await renderToPng(spec, {
  catalog,
  registry: imageRegistry,
  width: 1200,
  height: 630,
})
```

---

## @json-render/remotion

### `&lt;JsonRenderComposition&gt;` Component

```tsx
import { JsonRenderComposition } from '@json-render/remotion'

<JsonRenderComposition
  spec={spec}                    // JsonRenderSpec
  catalog={catalog}              // Catalog from defineCatalog()
  registry={remotionRegistry}    // Registry with Remotion animations
  fps={30}                       // Frames per second
  durationInFrames={150}         // Total duration
  width={1920}                   // Video width
  height={1080}                  // Video height
/>
```

### Remotion Registry with Animations

```tsx
import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion'

const remotionRegistry = {
  Heading: ({ text, level }) => {
    const frame = useCurrentFrame()
    const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' })
    return <h1 style={{ opacity, fontSize: level === 'h1' ? 48 : 36 }}>{text}</h1>
  },
}
```

---

## Common Options (all renderers)

| Option | Type | Description |
|--------|------|-------------|
| `catalog` | `Catalog` | Required. Zod-typed component definitions |
| `registry` | `CatalogComponents` | Required. Platform-specific component map |
| `spec` | `JsonRenderSpec` | Required. Flat-tree JSON/YAML spec |
| `onError` | `(err: Error) => void` | Optional. Error handler |


### Target Comparison

# Target Comparison Matrix

## All Targets — Capabilities Overview

| Target | Package | Output | Server-Side | Streaming | File Output | Sizing |
|--------|---------|--------|-------------|-----------|-------------|--------|
| React | `@json-render/react` | JSX | No (client) | Yes (progressive) | No | Responsive |
| Vue | `@json-render/vue` | Vue components | SSR supported | Yes | No | Responsive |
| Svelte | `@json-render/svelte` | Svelte components | SSR supported | Yes | No | Responsive |
| React Native | `@json-render/react-native` | Native views | No | Yes | No | Flex-based |
| PDF | `@json-render/react-pdf` | Buffer/File/Stream | Yes | Via stream | Yes | Fixed (A4, Letter) |
| Email | `@json-render/react-email` | HTML string | Yes | No | No | 600px max-width |
| Remotion | `@json-render/remotion` | MP4/WebM | Yes (render) | No | Yes | Fixed (px) |
| Image | `@json-render/image` | SVG/PNG | Yes | No | Yes (buffer) | Fixed (px) |
| YAML | `@json-render/yaml` | YAML string | Yes | No | No | N/A |
| MCP | `@json-render/mcp` | Sandboxed iframe | Yes | Yes | No | Constrained |
| 3D | `@json-render/react-three-fiber` | Three.js canvas | No | No | No | Canvas-based |
| Codegen | `@json-render/codegen` | TypeScript/JSX | Yes | No | Yes | N/A |

## Registry Component Primitives by Target

| Target | Base Elements | Styling | Layout |
|--------|--------------|---------|--------|
| React | `div`, `span`, `h1`-`h6`, `p`, `img` | className, CSS modules, Tailwind | Flexbox, Grid, any CSS |
| PDF | `View`, `Text`, `Image` (react-pdf) | StyleSheet.create() | Flexbox only |
| Email | `Section`, `Text`, `Heading`, `Button` (react-email) | Inline styles | Table-based (email compat) |
| Image | `div`, `span`, `img` (Satori subset) | Inline styles only | Flexbox only (limited) |
| Remotion | `div`, `span` + Remotion primitives | Inline styles, CSS | Flexbox, absolute positioning |
| React Native | `View`, `Text`, `Image` (RN) | StyleSheet.create() | Flexbox only |

## Limitations by Target

### PDF (`@json-render/react-pdf`)
- No CSS grid — flexbox only
- No className — use `StyleSheet.create()`
- No `<div>`/`<span>` — must use `&lt;View&gt;`/`&lt;Text&gt;`
- Font embedding required for custom fonts
- No interactive elements (buttons, links are display-only)

### Email (`@json-render/react-email`)
- 600px max-width (email client constraint)
- Table-based layout for Outlook compatibility
- Limited CSS support (no flexbox in Outlook, no grid)
- Inline styles only for maximum compatibility
- No JavaScript interactivity

### Image (`@json-render/image` / Satori)
- No CSS grid
- Limited flexbox (no `flex-wrap`, limited `align-items`)
- No `box-shadow`
- No CSS animations or transitions
- No external stylesheets — inline styles only
- `position: absolute` has limited nesting support
- Custom fonts must be loaded as ArrayBuffer

### Remotion (`@json-render/remotion`)
- No user interaction (pre-rendered video)
- Rendering is CPU-intensive — use cloud rendering for production
- Must specify exact `durationInFrames` and `fps`
- All animations must be frame-based (`useCurrentFrame`)

### React Native (`@json-render/react-native`)
- No `div`/`span` — use `View`/`Text`
- No CSS grid — flexbox only
- No className — use `StyleSheet.create()`
- Platform-specific behavior (iOS vs Android)

## Performance Characteristics

| Target | Render Time | Memory | CPU |
|--------|-------------|--------|-----|
| React | &lt;50ms (client) | Low | Low |
| PDF (buffer) | 200-500ms | Medium | Medium |
| PDF (stream) | 100-300ms start | Low | Medium |
| Email | 50-100ms | Low | Low |
| Image (SVG) | 100-200ms | Low | Low |
| Image (PNG) | 200-400ms | Medium | Medium |
| Remotion | 10-60s (full render) | High | High |
| Codegen | 50-100ms | Low | Low |

## When to Use Multiple Targets

Common multi-target combinations:

| Use Case | Targets | Example |
|----------|---------|---------|
| Dashboard + PDF export | React + PDF | Web dashboard with "Download PDF" button |
| Dashboard + email digest | React + Email | Web view + weekly email summary |
| Blog + social sharing | React + Image | Blog post + OG image preview |
| Product page + demo | React + Remotion | Landing page + demo video |
| Full marketing suite | React + PDF + Email + Image + Remotion | All surfaces from one spec |


### Upstream Email

&lt;!-- SYNCED from vercel-labs/json-render (skills/react-email/SKILL.md) --&gt;
&lt;!-- Hash: 8b376b37da07dd68944e72e0e322a272571f39271e5b08b6efd437db201470c9 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# @json-render/react-email

React Email renderer that converts JSON specs into HTML or plain-text email output.

## Quick Start

```typescript
import { renderToHtml } from "@json-render/react-email";
import { schema, standardComponentDefinitions } from "@json-render/react-email";
import { defineCatalog } from "@json-render/core";

const catalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});

const spec = {
  root: "html-1",
  elements: {
    "html-1": { type: "Html", props: { lang: "en", dir: "ltr" }, children: ["head-1", "body-1"] },
    "head-1": { type: "Head", props: {}, children: [] },
    "body-1": {
      type: "Body",
      props: { style: { backgroundColor: "#f6f9fc" } },
      children: ["container-1"],
    },
    "container-1": {
      type: "Container",
      props: { style: { maxWidth: "600px", margin: "0 auto", padding: "20px" } },
      children: ["heading-1", "text-1"],
    },
    "heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
    "text-1": { type: "Text", props: { text: "Thanks for signing up." }, children: [] },
  },
};

const html = await renderToHtml(spec);
```

## Spec Structure (Element Tree)

Same flat element tree as `@json-render/react`: `root` key plus `elements` map. Root must be `Html`; children of `Html` should be `Head` and `Body`. Use `Container` (e.g. max-width 600px) inside `Body` for client-safe layout.

## Creating a Catalog and Registry

```typescript
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry, renderToHtml } from "@json-render/react-email";
import { standardComponentDefinitions } from "@json-render/react-email/catalog";
import { Container, Heading, Text } from "@react-email/components";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Alert: {
      props: z.object({
        message: z.string(),
        variant: z.enum(["info", "success", "warning"]).nullable(),
      }),
      slots: [],
      description: "A highlighted message block",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Alert: ({ props }) => (
      <Container style={{ padding: 16, backgroundColor: "#eff6ff", borderRadius: 8 }}>
        <Text style={{ margin: 0 }}>{props.message}</Text>
      </Container>
    ),
  },
});

const html = await renderToHtml(spec, { registry });
```

## Server-Side Render APIs

| Function | Purpose |
|----------|---------|
| `renderToHtml(spec, options?)` | Render spec to HTML email string |
| `renderToPlainText(spec, options?)` | Render spec to plain-text email string |

`RenderOptions`: `registry`, `includeStandard` (default true), `state` (for `$state` / `$cond`).

## Visibility and State

Supports `visible` conditions, `$state`, `$cond`, repeat (`repeat.statePath`), and the same expression syntax as `@json-render/react`. Use `state` in `RenderOptions` when rendering server-side so expressions resolve.

## Server-Safe Import

Import schema and catalog without React or `@react-email/components`:

```typescript
import { schema, standardComponentDefinitions } from "@json-render/react-email/server";
```

## Key Exports

| Export | Purpose |
|--------|---------|
| `defineRegistry` | Create type-safe component registry from catalog |
| `Renderer` | Render spec in browser (e.g. preview); use with `JSONUIProvider` for state/actions |
| `createRenderer` | Standalone renderer component with state/actions/validation |
| `renderToHtml` | Server: spec to HTML string |
| `renderToPlainText` | Server: spec to plain-text string |
| `schema` | Email element schema |
| `standardComponents` | Pre-built component implementations |
| `standardComponentDefinitions` | Catalog definitions (Zod props) |

## Sub-path Exports

| Path | Purpose |
|------|---------|
| `@json-render/react-email` | Full package |
| `@json-render/react-email/server` | Schema and catalog only (no React) |
| `@json-render/react-email/catalog` | Standard component definitions and types |
| `@json-render/react-email/render` | Render functions only |

## Standard Components

All components accept a `style` prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.

### Document structure

| Component | Description |
|-----------|-------------|
| `Html` | Root wrapper (lang, dir). Children: Head, Body. |
| `Head` | Email head section. |
| `Body` | Body wrapper; use `style` for background. |

### Layout

| Component | Description |
|-----------|-------------|
| `Container` | Constrain width (e.g. max-width 600px). |
| `Section` | Group content; table-based for compatibility. |
| `Row` | Horizontal row. |
| `Column` | Column in a Row; set width via style. |

### Content

| Component | Description |
|-----------|-------------|
| `Heading` | Heading text (as: h1–h6). |
| `Text` | Body text. |
| `Link` | Hyperlink (text, href). |
| `Button` | CTA link styled as button (text, href). |
| `Image` | Image from URL (src, alt, width, height). |
| `Hr` | Horizontal rule. |

### Utility

| Component | Description |
|-----------|-------------|
| `Preview` | Inbox preview text (inside Html). |
| `Markdown` | Markdown content as email-safe HTML. |

## Email Best Practices

- Keep width constrained (e.g. Container max-width 600px).
- Use inline styles or React Email's style props; many clients strip `&lt;style&gt;` blocks.
- Prefer table-based layout (Section, Row, Column) for broad client support.
- Use absolute URLs for images; many clients block relative or cid: references in some contexts.
- Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.


### Upstream Image

&lt;!-- SYNCED from vercel-labs/json-render (skills/image/SKILL.md) --&gt;
&lt;!-- Hash: fc6469e1592a86d4d92058b81704023394ab8cbdd9f422d541ad3a752f2a0e42 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# @json-render/image

Image renderer that converts JSON specs into SVG and PNG images using Satori.

## Quick Start

```typescript
import { renderToPng } from "@json-render/image/render";
import type { Spec } from "@json-render/core";

const spec: Spec = {
  root: "frame",
  elements: {
    frame: {
      type: "Frame",
      props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" },
      children: ["heading"],
    },
    heading: {
      type: "Heading",
      props: { text: "Hello World", level: "h1", color: "#ffffff" },
      children: [],
    },
  },
};

const png = await renderToPng(spec, {
  fonts: [{ name: "Inter", data: fontData, weight: 400, style: "normal" }],
});
```

## Using Standard Components

```typescript
import { defineCatalog } from "@json-render/core";
import { schema, standardComponentDefinitions } from "@json-render/image";

export const imageCatalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});
```

## Adding Custom Components

```typescript
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Badge: {
      props: z.object({ label: z.string(), color: z.string().nullable() }),
      slots: [],
      description: "A colored badge label",
    },
  },
});
```

## Standard Components

| Component | Category | Description |
|-----------|----------|-------------|
| `Frame` | Root | Root container. Defines width, height, background. Must be root. |
| `Box` | Layout | Container with padding, margin, border, absolute positioning |
| `Row` | Layout | Horizontal flex layout |
| `Column` | Layout | Vertical flex layout |
| `Heading` | Content | h1-h4 heading text |
| `Text` | Content | Body text with full styling |
| `Image` | Content | Image from URL |
| `Divider` | Decorative | Horizontal line separator |
| `Spacer` | Decorative | Empty vertical space |

## Key Exports

| Export | Purpose |
|--------|---------|
| `renderToSvg` | Render spec to SVG string |
| `renderToPng` | Render spec to PNG buffer (requires `@resvg/resvg-js`) |
| `schema` | Image element schema |
| `standardComponents` | Pre-built component registry |
| `standardComponentDefinitions` | Catalog definitions for AI prompts |

## Sub-path Exports

| Export | Description |
|--------|-------------|
| `@json-render/image` | Full package: schema, components, render functions |
| `@json-render/image/server` | Schema and catalog definitions only (no React/Satori) |
| `@json-render/image/catalog` | Standard component definitions and types |
| `@json-render/image/render` | Render functions only |


### Upstream Pdf

&lt;!-- SYNCED from vercel-labs/json-render (skills/react-pdf/SKILL.md) --&gt;
&lt;!-- Hash: 42d7d378733e0c1e6ff10ba788dc77c32b36b2f28841f1d6aa671e44ed9ad0be --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# @json-render/react-pdf

React PDF renderer that generates PDF documents from JSON specs using `@react-pdf/renderer`.

## Installation

```bash
npm install @json-render/core @json-render/react-pdf
```

## Quick Start

```typescript
import { renderToBuffer } from "@json-render/react-pdf";
import type { Spec } from "@json-render/core";

const spec: Spec = {
  root: "doc",
  elements: {
    doc: { type: "Document", props: { title: "Invoice" }, children: ["page"] },
    page: {
      type: "Page",
      props: { size: "A4" },
      children: ["heading", "table"],
    },
    heading: {
      type: "Heading",
      props: { text: "Invoice #1234", level: "h1" },
      children: [],
    },
    table: {
      type: "Table",
      props: {
        columns: [
          { header: "Item", width: "60%" },
          { header: "Price", width: "40%", align: "right" },
        ],
        rows: [
          ["Widget A", "$10.00"],
          ["Widget B", "$25.00"],
        ],
      },
      children: [],
    },
  },
};

const buffer = await renderToBuffer(spec);
```

## Render APIs

```typescript
import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf";

// In-memory buffer
const buffer = await renderToBuffer(spec);

// Readable stream (pipe to HTTP response)
const stream = await renderToStream(spec);
stream.pipe(res);

// Direct to file
await renderToFile(spec, "./output.pdf");
```

All render functions accept an optional second argument: `\{ registry?, state?, handlers? \}`.

## Standard Components

| Component | Description |
|-----------|-------------|
| `Document` | Top-level PDF wrapper (must be root) |
| `Page` | Page with size (A4, LETTER), orientation, margins |
| `View` | Generic container (padding, margin, background, border) |
| `Row`, `Column` | Flex layout with gap, align, justify |
| `Heading` | h1-h4 heading text |
| `Text` | Body text (fontSize, color, weight, alignment) |
| `Image` | Image from URL or base64 |
| `Link` | Hyperlink with text and href |
| `Table` | Data table with typed columns and rows |
| `List` | Ordered or unordered list |
| `Divider` | Horizontal line separator |
| `Spacer` | Empty vertical space |
| `PageNumber` | Current page number and total pages |

## Custom Catalog

```typescript
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry, renderToBuffer } from "@json-render/react-pdf";
import { standardComponentDefinitions } from "@json-render/react-pdf/catalog";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    Badge: {
      props: z.object({ label: z.string(), color: z.string().nullable() }),
      slots: [],
      description: "A colored badge label",
    },
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Badge: ({ props }) => (
      <View style={{ backgroundColor: props.color ?? "#e5e7eb", padding: 4 }}>
        <Text>{props.label}</Text>
      </View>
    ),
  },
});

const buffer = await renderToBuffer(spec, { registry });
```

## External Store (Controlled Mode)

Pass a `StateStore` for full control over state:

```typescript
import { createStateStore } from "@json-render/react-pdf";

const store = createStateStore({ invoice: { total: 100 } });
store.set("/invoice/total", 200);
```

## Server-Safe Import

Import schema and catalog without pulling in React:

```typescript
import { schema, standardComponentDefinitions } from "@json-render/react-pdf/server";
```


### Upstream Remotion

&lt;!-- SYNCED from vercel-labs/json-render (skills/remotion/SKILL.md) --&gt;
&lt;!-- Hash: 59d63ac06729795f0981fe0cfadf6c1c34454be0329a34f2d36c9be2391fbbe9 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# @json-render/remotion

Remotion renderer that converts JSON timeline specs into video compositions.

## Quick Start

```typescript
import { Player } from "@remotion/player";
import { Renderer, type TimelineSpec } from "@json-render/remotion";

function VideoPlayer({ spec }: { spec: TimelineSpec }) {
  return (
    <Player
      component={Renderer}
      inputProps={{ spec }}
      durationInFrames={spec.composition.durationInFrames}
      fps={spec.composition.fps}
      compositionWidth={spec.composition.width}
      compositionHeight={spec.composition.height}
      controls
    />
  );
}
```

## Using Standard Components

```typescript
import { defineCatalog } from "@json-render/core";
import {
  schema,
  standardComponentDefinitions,
  standardTransitionDefinitions,
  standardEffectDefinitions,
} from "@json-render/remotion";

export const videoCatalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
  transitions: standardTransitionDefinitions,
  effects: standardEffectDefinitions,
});
```

## Adding Custom Components

```typescript
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...standardComponentDefinitions,
    MyCustomClip: {
      props: z.object({ text: z.string() }),
      type: "scene",
      defaultDuration: 90,
      description: "My custom video clip",
    },
  },
});

// Pass custom component to Renderer
<Player
  component={Renderer}
  inputProps={{
    spec,
    components: { MyCustomClip: MyCustomComponent },
  }}
/>
```

## Timeline Spec Structure

```json
{
  "composition": { "id": "video", "fps": 30, "width": 1920, "height": 1080, "durationInFrames": 300 },
  "tracks": [{ "id": "main", "name": "Main", "type": "video", "enabled": true }],
  "clips": [
    { "id": "clip-1", "trackId": "main", "component": "TitleCard", "props": { "title": "Hello" }, "from": 0, "durationInFrames": 90 }
  ],
  "audio": { "tracks": [] }
}
```

## Standard Components

| Component | Type | Description |
|-----------|------|-------------|
| `TitleCard` | scene | Full-screen title with subtitle |
| `TypingText` | scene | Terminal-style typing animation |
| `ImageSlide` | image | Full-screen image display |
| `SplitScreen` | scene | Two-panel comparison |
| `QuoteCard` | scene | Quote with attribution |
| `StatCard` | scene | Animated statistic display |
| `TextOverlay` | overlay | Text overlay |
| `LowerThird` | overlay | Name/title overlay |

## Key Exports

| Export | Purpose |
|--------|---------|
| `Renderer` | Render spec to Remotion composition |
| `schema` | Timeline schema |
| `standardComponents` | Pre-built component registry |
| `standardComponentDefinitions` | Catalog definitions |
| `useTransition` | Transition animation hook |
| `ClipWrapper` | Wrap clips with transitions |

