# KineticCaption

Move an opening headline into a smaller caption to make room for the product.

Page: https://20cuts.com/docs/components/kinetic-caption

## Installation

Run from your product repository:

```sh
npx -y https://20cuts.com/releases/v0.3.0/20cuts-create-0.3.0.tgz video/20cuts
cd video/20cuts
npm ci
npm run studio
```

The installer includes this component, its source dependencies, runnable examples,
and skills for Codex and Claude Code. If 20cuts is already installed, import the
component from its existing source directory.

## Usage

```tsx
import { KineticCaption } from "./system/motion";

export default function VideoScene() {
  return (
    <KineticCaption text="Bring your work into focus." at={0.3} recedeAt={1.8} captionScale={0.42} />
  );
}
```


## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| text (required) | string | None | Text to display. |
| at | number | 0 | Start time in seconds within the current Remotion Sequence. |
| duration | number | 0.55 | Entrance duration for each word, in seconds. |
| recedeAt | number | 1.7 | Time the headline starts moving into its caption position. |
| recedeDuration | number | 0.6 | Caption movement duration in seconds. |
| captionScale | number | 0.42 | Final caption scale relative to the headline. |
| captionY | number | height * 0.88 | Final vertical center in composition pixels. |
| style | CSSProperties | None | Styles applied to the component wrapper. |

The headline starts centered at half the composition height. With captionY omitted, its final center is height × 0.88.

## Complete example

This is the source of the fictional component demonstration. Replace its content
with supported behavior and assets from your product.

```tsx
import React, { type CSSProperties, type ReactNode } from "react";

import { AbsoluteFill } from "remotion";

import { AppFrame, FadeIn, KineticCaption } from "./system";

const ink = "#141414", lime = "#b6f36b";

const center: CSSProperties = { display: "flex", alignItems: "center", justifyContent: "center" };

const card: CSSProperties = { background: "#fff", border: "1px solid #ddd", borderRadius: 20, padding: 32 };

function Stage({ children, dark = false, style }: {
    children: ReactNode;
    dark?: boolean;
    style?: CSSProperties;
}) {
    return <AbsoluteFill style={{ fontFamily: "Arial, Helvetica, sans-serif", color: dark ? "#fff" : ink, background: dark ? ink : "#f7f7f5", ...center, ...style }}>{children}</AbsoluteFill>;
}

function Dashboard({ compact = false, emphasizeMetric = false }: {
    compact?: boolean;
    emphasizeMetric?: boolean;
}) {
    return <div style={{ padding: compact ? 24 : 36, display: "grid", gap: 26, color: ink }}>
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}><strong style={{ fontSize: 30, letterSpacing: "-0.035em" }}>Project overview</strong><span style={{ background: lime, padding: "10px 18px", borderRadius: 8, fontSize: 16 }}>New project +</span></div>
    <div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 16 }}>{[["In progress", "08"], ["Completed", "24"], ["This week", "+12%"]].map(([label, value], i) => <div key={label} style={{ ...card, padding: 20, background: "#fafafa", outline: emphasizeMetric && i === 2 ? `4px solid ${lime}` : undefined }}><div style={{ fontSize: 16, color: "#666" }}>{label}</div><div style={{ fontSize: 42, marginTop: 12, letterSpacing: "-0.05em" }}>{value}</div></div>)}</div>
    <div style={{ display: "flex", height: compact ? 110 : 150, alignItems: "flex-end", gap: 14, borderBottom: "1px solid #ddd" }}>{[24, 42, 35, 68, 54, 78, 96, 74, 84, 100].map((n, i) => <div key={i} style={{ flex: 1, height: `${n}%`, background: i === 9 ? lime : "#e4e5e0", borderRadius: "8px 8px 0 0" }}/>)}</div>
  </div>;
}

export function KineticCaptionExample() { return <Stage><FadeIn at={2.2} duration={0.8}><AppFrame width={800} height={450}><Dashboard compact/></AppFrame></FadeIn><KineticCaption text="Bring your work into focus." at={0.3} recedeAt={1.8} captionY={645} style={{ fontSize: 78 }}/></Stage>; }
```

Save it as src/KineticCaptionExample.tsx. This demonstration uses
1280×720, 30fps and 180 frames.
For a separate entry point, replace src/index.tsx with this registration, then
run npm run studio:

```tsx
import { Composition, registerRoot } from "remotion";
import { KineticCaptionExample } from "./KineticCaptionExample";

registerRoot(() => (
  <Composition
    id="KineticCaptionExample"
    component={KineticCaptionExample}
    width={1280}
    height={720}
    fps={30}
    durationInFrames={180}
  />
));
```

## Source

- https://20cuts.com/animation-system/library/source/motion/typography.tsx
- https://20cuts.com/animation-system/library/source/motion/core.ts

## Agent instructions

Use the 20cuts KineticCaption component in this project's Remotion composition. Let the headline establish the idea, then recede it toward the bottom at 1.8 seconds while the product takes center stage. Keep motion frame-driven and put timing in seconds relative to the current Sequence. Use the product's actual copy and assets. Preview the composition before rendering.
