# The authoring model

## Two formats, mixed freely
**MDX: prose slides**
Write Markdown; element tags (`h1`, `p`, `ul`, `code`, …) map to themed components automatically. Best for text-heavy slides. Fenced code is syntax-highlighted at build time.

**TSX: interactive slides**
A default-exported React component. Best for anything animated, data-driven, or that places a `<Plugin>`. Full access to Motion, the engine, and your own components.

Both compile to the same thing; pick per slide and mix them in one deck.

## The fixed canvas

Every slide is authored on a logical **1280×720** canvas (`STAGE_W × STAGE_H`). The engine's `ScaledStage` scales that canvas to fit any screen, centered and letterboxed, so a slide looks identical on a laptop, a projector, and a phone (just at a different scale). You never write responsive breakpoints for slide content. You design once at 1280×720.

`SlideFrame` gives each slide a brand background, an animated atmosphere, and a padded content area. Break out with absolute positioning for full-bleed visuals.

## Themed MDX elements

In MDX you write plain Markdown and the look comes from the brand. The mapping (in `@liebstoeckel/components`):

| Markdown | Renders as |
| --- | --- |
| `# H1` | display serif, `text-text` |
| `## H2` | `text-primary` heading |
| paragraph | `font-body`, `text-muted` |
| list item | accent bullet (`▹`) |
| `` `inline code` `` | a themed pill |
| ```` ```ts ```` block | Shiki-highlighted, brand-bound colors |
**Tip:** Need a component inside MDX? Import it at the top of the `.mdx` file like any module: `import { Plugin } from "@liebstoeckel/engine"`.

## Persistent stateful elements

[Magic Move](https://docs.liebstoeckel.app/guides/your-first-deck/#magic-move) re-renders content, which would reset a stateful element (an `<iframe>`, a `<video>`, a running widget). For those, the engine provides a **persistent layer**: render the element once at the deck root and project it into slides via a `<Slot>`. It keeps its internal state while traveling between slides.

The two are complements: `Magic` morphs *different stateless elements* into one another (declared inline per slide); `<Slot>` carries *one stateful element* across slides (defined once). Reach for `<Slot>` when state must survive, `Magic` for a stateless flourish.

```tsx title="main.tsx"
<Present
  slides={[...]}
  persistent={[{ id: "live", render: () => <LiveIframe /> }]}
/>
```

```mdx title="a slide"
## The clock keeps running →

<Slot id="live" className="mt-10 h-56 w-full rounded-2xl border border-surface" />
```

Move the `<Slot id="live">` to a different position on the next slide and the live element animates to its new home without reloading.

### How it behaves between slides

The element is positioned onto the slot on the **current** slide, and how it gets there depends on where you came from:

- **Travel**: both the slide you left *and* the one you entered have a `<Slot>` with the same `id`. The element **springs** from the old position/size to the new one, state intact. This is the headline trick: put the slot in two consecutive slides at different spots to make a live widget glide across.
- **Appear**: you arrive from a slide that has **no** matching slot (or it's the first time it shows). The element **snaps** into the slot and only fades in. It won't fly in from wherever it last was.
- **Leave**: the next slide has no matching slot. The element **fades out in step with the slide transition** (it doesn't linger after).

It's positioned in the deck's logical canvas space, so it lines up with the slot at any stage scale (fullscreen, windowed, or mobile). Reloading is never triggered by any of these: the element is rendered once and never unmounts.

## Slide transitions

How one slide gives way to the next is configurable. Set a deck-wide default on `Present`, and override any individual slide with an `export const transition`:

```tsx title="main.tsx"
<Present slides={[...]} transition="slide" />   {/* deck default; omit for "fade" */}
```

```tsx title="a slide"
export const transition = "zoom";               // this slide only
```

Built-in presets: **`fade`** (default), `blur`, `slide` (directional push, mirrors on back-nav), `zoom`, `none`. Need something bespoke? Pass a custom spec instead of a name:

```tsx
export const transition = {
  variants: { enter: { opacity: 0, y: 40 }, center: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -40 } },
  transition: { duration: 0.5 },
};
```

Transitions respect `prefers-reduced-motion` (they collapse to a tiny fade), and don't affect persistent-layer elements: those travel above the swap untouched.

On **mobile** (touch / coarse pointer) transitions are **off by default**. The stage is heavily down-scaled there, so cuts feel snappier and avoid jank. Opt back in with the `mobileTransitions` escape hatch:

```tsx title="main.tsx"
<Present slides={[...]} transition="slide" mobileTransitions />
```

[Animated code](https://docs.liebstoeckel.app/guides/animated-code/)