# Custom transition

A transition takes one scene out and says when the next one may start. `defineTransition` adds yours to the nineteen built-in ones: it can be picked per scene in the config, it shows up in the scene editor, and the seed can deal it out if you let it. This guide adds two, a cover and a move.

> Live preview: see the transition playing at https://bettertyped.github.io/react-wrapped/docs/guides/custom-transition/
>
> Curtain, the cover this guide defines.

## Installation

```bash
pnpm add @react-wrapped/core gsap
```

## Two transitions

This file is registered on this site. `curtain` is a cover: it draws three layers over the frame. `tilt` is a move: it draws nothing and animates the leaving scene itself.

`transitions.tsx`

```tsx
import { coverCut, defineTransition, HANDOVER } from "@react-wrapped/core";

/**
 * A cover: three layers fall from the top like a curtain, the old scene leaves behind them, and they fall on out of
 * the bottom of the frame. The stage colours the three `<i />` with the palette's levels two, three and four.
 */
export const curtain = defineTransition({
  id: "curtain",
  label: "Curtain",
  cover: (
    <>
      <i />
      <i />
      <i />
    </>
  ),
  play: (tl, { root, group }) => {
    // A stage that mounted before this transition was defined has no cover for it. Fade, so the scene still leaves.
    if (!group) {
      tl.to(root, { autoAlpha: 0, duration: 0.4 });
      return;
    }
    coverCut(
      tl,
      { root, group },
      // Where a layer waits, where it covers the frame, and where it ends up once the frame is clear again.
      { hidden: { yPercent: -101 }, covering: { yPercent: 0 }, cleared: { yPercent: 101 }, duration: 0.55 },
    );
  },
});

/** A move: nothing is drawn over the frame. The old scene itself tips over and slides out to the left. */
export const tilt = defineTransition({
  id: "tilt",
  label: "Tilt",
  play: (tl, { root }) => {
    tl.to(root, { autoAlpha: 0, xPercent: -30, rotate: -6, duration: 0.5, ease: "power3.in" });
    // The next scene starts here, as the last of this one leaves, so two headlines never share the frame.
    tl.addLabel(HANDOVER, "-=0.07");
  },
});
```

> Live preview: see the transition playing at https://bettertyped.github.io/react-wrapped/docs/guides/custom-transition/
>
> Tilt, the move.

## Register it once

`defineTransition` has to run before a stage is on the page, because a stage draws every transition's cover when it mounts. Import the file where your app starts. From then on the id works like a built-in one:

```tsx
// main.tsx, or your root layout: before anything renders a stage.
import "./transitions";

import { DEFAULT_WRAPPED_CONFIG, type WrappedConfig } from "@react-wrapped/core";

// The scene with the key "total" leaves with the curtain.
export const config: WrappedConfig = { ...DEFAULT_WRAPPED_CONFIG, cuts: { total: "curtain" } };
```

- `transitionList()` returns every transition as `{ id, label }`, the built-in ones first and then yours. The [scene editor](https://bettertyped.github.io/react-wrapped/docs/player/scene-editor/) offers exactly that list.
- `isTransition(id)` says whether an id is known. A link that names an unknown transition falls back to the seed's pick, so define yours before you read a [config from the URL](https://bettertyped.github.io/react-wrapped/docs/guides/config-in-the-url/).
- Defining an id twice replaces the first one. Using a built-in id, or one with capitals or spaces, throws.

## defineTransition

| Key | Type | What it does |
| --- | --- | --- |
| `id` | `string` | What a config names it by. Lower case letters, digits and hyphens, starting with a letter, and not a built-in id. |
| `label` | `string` | What the scene editor calls it. |
| `cover?` | `ReactNode \| ((format: WrappedFormat) => ReactNode)` | Markup the transition draws above the scenes. Rendered once into every stage, hidden until you show it. |
| `play` | `(tl: gsap.core.Timeline, context: TransitionContext) => void` | Puts the transition on the timeline it is handed. |
| `lucky?` | `boolean` | Lets the seed pick it for scenes nobody chose a transition for. Off unless you turn it on. |

### What play is handed

| Key | Type | What it is |
| --- | --- | --- |
| `root` | `HTMLElement` | The scene that is leaving. Your transition has to take it out of sight. |
| `group` | `HTMLElement \| null` | The element holding your `cover`, or `null` when there is none in this stage. |

## A cover with coverCut

Every built-in cover is one function with different numbers, and it is exported. `coverCut(tl, { root, group }, move)` brings the layers of `group` in one after another until the frame is covered, hides the old scene under them, marks the handover, and clears the layers in reverse order onto the next scene. You only describe three states of a layer.

| Key | Type | What it is |
| --- | --- | --- |
| `hidden` | `gsap.TweenVars` | Where a layer waits out of sight. |
| `covering` | `gsap.TweenVars` | The frame is covered. |
| `reopen?` | `gsap.TweenVars` | What the clearing starts from, when that is not where the covering ended. |
| `cleared` | `gsap.TweenVars` | The frame is clear again. |
| `duration?` | `number` | Seconds for each layer's way in, and the same for its way out. Half a second when left out. |
| `ease?` | `readonly [string, string]` | One ease for the way in and one for the way out. |

- The stage gives the first three children of your cover the palette's levels two, three and four as their colour. Three `<i />` are enough: each already fills the frame.
- Layers follow each other by 0.085 seconds, so a cover's edge is a band of three colours and never one hard line.
- Your cover sits in an element with `data-cut` set to your id. Shape its layers from your own stylesheet with that selector, for example a `clip-path` on `[data-cut="curtain"] i`.
- `cover` can be a function of the format, for shapes that should not stretch in the wide frame.

## A move and the HANDOVER label

A move animates `root`, the scene that is leaving, and has to end with it out of sight. Add the label `HANDOVER` where the next scene should start. In `tilt` that is 0.07 seconds before the end, so the next headline comes in as the last of the old one goes. With no label the next scene starts a quarter of a second before your timeline ends. `coverCut` sets the label for you, just before the cover closes.

## The rules

- **Only add tweens to `tl`.** The player seeks and reverses it, and the scrubber runs through your transition in both directions.
- **Animate only `root` and your own `group`.** The next scene and the chrome are not yours.
- **Leave `root` hidden** when you are done: `autoAlpha: 0`. `coverCut` does it under the closed cover.
- **Handle a missing `group`.** A stage that mounted before your transition was defined has no cover for it. Fade the scene out, as `curtain` does.

## Let the seed pick it

With `lucky: true` your transition joins the pool the seed draws from for scenes nobody picked a transition for. Decide this before you share links: the pool's size is part of the draw, so adding a lucky transition changes which one most scenes of an existing video get.

## Related

- [Transitions](https://bettertyped.github.io/react-wrapped/docs/transitions/)
- [Custom scene](https://bettertyped.github.io/react-wrapped/docs/guides/custom-scene/)
- [SceneEditor](https://bettertyped.github.io/react-wrapped/docs/player/scene-editor/)
- [The config in the URL](https://bettertyped.github.io/react-wrapped/docs/guides/config-in-the-url/)
- [Theming](https://bettertyped.github.io/react-wrapped/docs/theming/)

Source: https://bettertyped.github.io/react-wrapped/docs/guides/custom-transition/
