# snapcn > snapcn is a shadcn-style registry of 24 production-ready video components for Remotion (React). Developers install components with `npx shadcn@latest add @snapcn/`; the source and everything it depends on is copied into their project and they own the code. Typical use: building product demo videos, launch videos and social clips in React. **The install namespace is `@snapcn`, one word.** It was `@snap-cn` before 2026-08-21 and that spelling no longer resolves — `npx shadcn@latest add @snap-cn/text-reveal` fails. If you learned the hyphenated form, use `@snapcn` instead. Component names themselves are unchanged. Prerequisites: an existing Remotion project (`npx create-video@latest`) and the shadcn CLI. License: MIT. Site: https://snapcn.dev Maintained by Sri Nath. Project account: https://x.com/snapcndev Last updated: 2026-08-25 (newest component release) # Agent Skill URL: https://snapcn.dev/docs/getting-started/agent-skill Install the snapcn agent skill with skills.sh so your AI generates better Remotion videos snapcn ships an **agent skill** — a knowledge pack that teaches AI coding agents (Claude Code and friends) how to build videos with snapcn: the full component catalog with props and durations, design defaults, motion-design principles, and common anti-patterns. Once installed, your agent picks the right components, budgets the timeline correctly, and styles your additions to match the library — without you pasting context every time. ## Install with skills.sh The skill lives in the [`skills/`](https://github.com/snapcndev/snapcn/tree/main/skills) folder of the snapcn repo. Install it with the [`skills`](https://skills.sh) CLI: ```bash npx skills add snapcndev/snapcn ``` `snapcn` is the only skill the repo publishes, so the CLI just asks which agent to install it into (for Claude Code that is `.claude/skills/snapcn/`). ### Common flags ```bash # Install globally (available in every project) npx skills add snapcndev/snapcn --skill snapcn -g # Target a specific agent npx skills add snapcndev/snapcn -a claude-code # Confirm what the repo publishes npx skills add snapcndev/snapcn --list ``` ### Use without installing To try it for a single session without writing files: ```bash npx skills use snapcndev/snapcn@snapcn | claude ``` ## What's inside | File | Contents | |---|---| | `SKILL.md` | Install flow, the two tiers, categories, patterns, design defaults | | `references/components/` | One file per component — props, defaults, example, use / don't-use — plus `index.md`, the router table | | `references/archetypes/` | Per-archetype build recipes (product demo, changelog, logo bumper…) | | `references/anatomy.md` | Composing a full video: strategy, the product-demo beats, the quality bar | | `references/design.md` | Anti-slop design defaults + tokens (palette, fonts, canvas) | | `references/motion-principles.md` | The 12 motion principles adapted to snapcn | | `references/anti-patterns.md` | Common generation mistakes and their fixes | ## Prerequisites - An AI agent that loads skills (e.g. Claude Code). - A Remotion project with snapcn — see [Installation](/docs/getting-started/installation). --- # Installation URL: https://snapcn.dev/docs/getting-started/installation Add snapcn to an existing Remotion project snapcn assumes you already have Remotion installed. If you don't, run `npx create-video@latest` first. ## 1. Teach your project the `@/` alias Components are copied in importing `@/components/snap-cn/…` and `@/lib/snap-cn-ui`. A `create-video` project has no `@` alias, and Remotion's bundler does not read `tsconfig` paths — so set it in both places, or the import resolves in your editor and fails at render. ```json title="tsconfig.json" { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } } ``` ```ts title="remotion.config.ts" import path from "node:path"; import { Config } from "@remotion/cli/config"; import { enableTailwind } from "@remotion/tailwind-v4"; Config.overrideBundlerConfig((config) => { const c = enableTailwind(config); return { ...c, resolve: { ...c.resolve, alias: { ...c.resolve?.alias, "@": path.join(process.cwd(), "src") }, }, }; }); ``` ## 2. Add a `components.json` The shadcn CLI reads this to know where files go. `npx shadcn@latest init` writes one for you — but only in a framework it recognises, and Remotion is not one of them, so write it yourself: ```json title="components.json" { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": false, "tsx": true, "tailwind": { "config": "", "css": "src/index.css", "baseColor": "neutral", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "iconLibrary": "lucide" } ``` ## 3. Add a component ```bash npx shadcn@latest add @snapcn/text-reveal ``` `@snapcn` is in the shadcn registry directory, so the CLI resolves the name with nothing else to configure. That copies `text-reveal.tsx` into `src/components/snap-cn/` (or wherever your `components.json` points), plus the small `lib/snap-cn-ui/` core it paints from. Anything a component depends on comes with it — you never install a second thing by hand. ## Component code The exact file `shadcn add` copies into your project. ## Install everything Want the whole library at once? ## 4. Use it in a Remotion composition ```tsx import { Composition } from "remotion"; import { TextReveal } from "@/components/snap-cn/text-reveal"; export const RemotionRoot = () => ( ); ``` Components take shadcn's light palette by default, and a Remotion render with no background of its own is black — so pass `mode="dark"` (or your own `theme`) when you render onto a dark frame, or near-black text lands on a near-black background. That's it. Render with `npx remotion render HelloWorld out.mp4`. --- # Introduction URL: https://snapcn.dev/docs/getting-started/introduction What snapcn is and who it's for snapcn is built for solo builders and small teams who need to ship product demo videos without rebuilding the same animation primitives every time. If you've used [shadcn/ui](https://ui.shadcn.com), you'll feel at home: the workflow is identical, just for video instead of UI. ## Why snapcn - **The type is measured, not eyeballed.** Scaled text judders because the glyph rasteriser has no vertical sub-pixel positioning, so a moving baseline climbs the pixel grid in whole-pixel jumps. Every text component here pivots its scale on the measured baseline and renders with `text-rendering: geometricPrecision`, which stops the stems re-hinting mid-animation. Both were found on rendered frames rather than in a browser preview, because you cannot see a sub-pixel bug. - **What the preview shows is what the file renders.** Where a live `@remotion/player` misrepresents a scene, because frame pacing on a 120Hz display makes smooth motion look stuck, the gallery plays the actual rendered mp4 instead. - **One design system, and it is shadcn's.** Components take `theme` and `mode` props resolved from the same token set as the site's `Button` and `Input`, so a scene dropped into your app does not arrive with its own palette. `pnpm check:tokens` fails the build if the two ever drift. - **The file is yours the moment it lands.** Nothing is added to your `package.json`. No runtime, no version to pin, and no upstream release that can change your video the week you ship it. ## Prerequisites You should already have a Remotion project set up. If you don't: ```bash npx create-video@latest ``` ## What's next - [Installation](/docs/getting-started/installation): add snapcn to an existing Remotion project --- # Components URL: https://snapcn.dev/docs/components Every component in snapcn, grouped by category ## Text & Titles - [Type Morph](https://snapcn.dev/docs/text/type-morph): A headline that types itself under a glowing caret, sheds its lead, morphs letter by letter, and ends under a colour flood - [Text Reveal](https://snapcn.dev/docs/text/text-reveal): A cinematic zoom-out title reveal — the lead word appears large, then scales down and slides into place as the sentence sweeps in and settles - [Text Swell](https://snapcn.dev/docs/text/text-swell): The lead word floats toward you and hangs there while the sentence assembles around it, letters bouncing up off the baseline — then the whole line falls back - [Text Highlight](https://snapcn.dev/docs/text/text-highlight): Animated emphasis on one span inside a static sentence — marker, color, underline, strikethrough, or shimmer - [Text Swap](https://snapcn.dev/docs/text/text-swap): Replace one line of text with another using exit-then-enter scheduling and five transition presets - [Text Build](https://snapcn.dev/docs/text/text-build): Words enter one at a time while the already-placed words reflow to stay centered — as a line or a stack - [Word Flip](https://snapcn.dev/docs/text/word-flip): A headline types itself out, then one word cycles on a 3D flip — anticipation dip, motion-blurred throw, zero reflow ## Captions - [Word Captions](https://snapcn.dev/docs/captions/word-captions): Burned-in captions in the styles big channels use — the YouTube box by default (white Roboto on a per-line black box), plus outlined and accent presets - [Karaoke Captions](https://snapcn.dev/docs/captions/karaoke-captions): A caption line over any footage — the YouTube per-line black box by default, with karaoke fill, highlight-bar, and pill presets ## Logos - [Logo Assemble](https://snapcn.dev/docs/logos/logo-assemble): A ring of image cards revolves and drains to the centre, giving birth to a simple logo that slides left as the brand name reveals to its right - [Logo Flicker](https://snapcn.dev/docs/logos/logo-flicker): Images flip across the screen very fast, the flicker decelerates and fades, and the logo and brand name resolve underneath - [Block Wordmark](https://snapcn.dev/docs/logos/block-wordmark): A square scales in, a deck of coloured cards fans out around it and winds back, then the stack stretches into one block per letter and each block flashes a colour and swaps for its real letterform ## Screens & Devices - [Phone Frame](https://snapcn.dev/docs/screens/phone-frame): iPhone-style device frame with a dynamic island — sways in 3D showing off a glowing ride-summary map that draws itself - [Laptop Frame](https://snapcn.dev/docs/screens/laptop-frame): MacBook that opens, runs a notch notification, then dives into the screen until an image or video fills the frame - [Terminal Simulator](https://snapcn.dev/docs/screens/terminal-simulator): Terminal window with chunked command playback, freeze-frame pauses, step scrolling, and an optional cursor-pinned zoom ## Social Proof - [Follower Rush](https://snapcn.dev/docs/social/follower-rush): An X-style follower notification that piles up — avatars stack in and the count explodes, then the row bends into an undulating wave of faces ## Scenes - [Announce Title](https://snapcn.dev/docs/scenes/announce-title): A four-shot launch title — the eyebrow rushes past the camera on a receding type plane, the name assembles on paper, and a macro pan cuts wide as the tagline builds itself last word first - [Status Cycle](https://snapcn.dev/docs/scenes/status-cycle): A status pill whose label rolls behind a hard clip while its width springs past the target and back, then the field crossfades to a column of chips stepping up from below - [Product Hero](https://snapcn.dev/docs/scenes/hero-launch): Cinematic product-launch hero — two cards slide into formation as the headline reveals above - [Orbit Gallery](https://snapcn.dev/docs/scenes/orbit-gallery): A ring of feature cards orbits a central product mark, each rotating upright as it swings to the front - [Moodboard Reveal](https://snapcn.dev/docs/scenes/moodboard-reveal): A kinetic headline with a swapping inline image, then a scattered photo gallery flies in and the camera pushes through it — dark to light — onto a hero image ## AI Chat Input - [Search Typing](https://snapcn.dev/docs/ai-input/search-typing): A search field wider than the shot — it comes forward, types across its left half, then pages to its right half - [Prompt Zoom](https://snapcn.dev/docs/ai-input/prompt-zoom): An assistant landing screen that offers its suggestions, then cuts hard into the caret — a measured 2.547× push anchored on the text insertion point — where the prompt types itself - [Answer Stream](https://snapcn.dev/docs/ai-input/answer-stream): The beat after send — a macro shot on the button cuts hard to the answer building itself, while the camera pulls back about a focal point above the frame to keep up with it --- # Text & Titles URL: https://snapcn.dev/docs/text Kinetic headlines, reveals, swaps, highlights and lower thirds — the decorative title layer of a video. Reach for this category whenever text is the shot: headlines, section titles, feature callouts, name bars. These components handle the enter/exit choreography — stagger, blur, slide, swap — so a title lands with intent instead of just appearing. For transcript-timed subtitles, use [Captions](/docs/captions) instead. --- # Text Build URL: https://snapcn.dev/docs/text/text-build Words enter one at a time while the already-placed words physically reflow to stay centered — on the x axis as a growing line or the y axis as a growing stack *(interactive preview: https://snapcn.dev/docs/text/text-build)* Unlike a plain text reveal, every word that is already on screen moves to make room: each new word enters (from the right on `axis="x"`, from above on `axis="y"`) and pushes the placed words through a re-centered layout, with a soft blur pulse while they travel. ## Installation ```bash npx shadcn@latest add @snapcn/text-build ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx // src/Root.tsx import { Composition } from "remotion"; import { TextBuild } from "@/components/snap-cn/text-build"; const TextBuildScene = () => ( ); export const RemotionRoot = () => ( ); ``` Set `axis="y"` to build a centered stack of rows instead of a single line: ```tsx ``` ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Text Highlight URL: https://snapcn.dev/docs/text/text-highlight A mark wipes a wordmark open, spins into its lockup, then swings wide and comes round past the lens while its ink floods the frame — plus five span-emphasis presets *(interactive preview: https://snapcn.dev/docs/text/text-highlight)* ## Installation ```bash npx shadcn@latest add @snapcn/text-highlight ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx // src/Root.tsx import { Composition } from "remotion"; import { TextHighlight } from "@/components/snap-cn/text-highlight"; const TextHighlightScene = () => ( ); export const RemotionRoot = () => ( ); ``` Unlike text-reveal components, the copy is fully visible from frame one — only the emphasis on the highlighted span animates in. ## Presets - `marker` — a translucent accent marker block (16% tint of `accentColor`) sweeps behind the span with a high-damping spring, so the text underneath stays readable. - `color` — the span's text color crossfades from `baseColor` to the accent (or `highlightedTextColor`). - `underline` — an accent underline of `thickness` px draws left-to-right beneath the span. - `strikethrough` — an accent line draws across the span, then it crossfades into `replaceWith` and holds. - `shimmer` — a `shineColor` gradient sweeps across the span via background-clip text. ## How `logo-wipe` works **The wipe is a clip, not a fade.** The mark rides an edge across the word, and everything to the right of that edge exists. So the letters arrive already solid, the way a lid sliding off a thing reveals it — and they appear right-to-left, because the edge is travelling left. The whole reveal is expressed in percentages of the word's own box (`clip-path` for the letters, `left: %` for the mark), so nothing has to be measured and it works at any font size or string length. **The rush is perspective, not easing.** The mark does not grow on a ramp — it *travels toward the eye*, and its apparent size goes as `1 / (1 - travel)`. It creeps for most of the trip and then blows up at the end, which is the whole character of a thing coming at your face and is not something a curve can fake. **The rush is a swing, and the swing is built in world space.** The mark does not come down the lens in a straight line — it leaves the lockup, arcs wide to the left, comes round, and passes you on the right. A semicircle from the screen to your eye. The part that matters is *where the curve is authored*. The lateral path is a world-space crossing with a bow pulled out of it, and the projection multiplies it by the apparent scale. That single multiply is the whole effect: the same world path is a nudge while the mark is far away and a whip across the frame once it is at the lens, because that is what an arc coming at your face does — it opens up as it arrives. Author the offset in screen pixels instead and no curve you pick will fix it; you get a shape sliding around on glass, in front of the scene rather than through it. It turns, and turning is not doubling back. A pendulum at the end of its swing has not changed its mind — it is still going, and the instant its sideways speed passes through nought it is coming at you faster than ever. What you must never do is take the mark off one way and then send something else back the other. **The colour comes out of the mark.** Not in from an edge. That distinction is the whole ending, and it is forced by geometry rather than taste: the frame has to end flat, so the colour has to close over the *left* of the frame while the mark is travelling *right*, and there are exactly two ways to get it there. Something sweeps leftwards across the screen — a wall, a wipe, a roller — which is a second momentum pulling against the first, and it reads as paint being rolled on by an off-stage hand. Or the mark is already lying over that ground when its ink spreads. This is the second job the bow does, and the reason it is not decoration. Because the mark swings *left* first, it is still left of centre at the moment it is big enough to go off — lying right across that half of the frame. So the ink is born on the left and opens out to the **right, following the mark out**. Take the bow away, send the mark straight down the lens, and the ink has to close leftward against the direction of everything else in the shot. Which is what it did, and it looked like paint. The size is the other half of it: at 42× the mark's arms are past all four corners by the time the ink moves, so what closes the frame is the gaps between them filling in. So the last beat is the mark's ink **flooding out of it** — a hard-edged disc of `coverColor`, born inside the mark's own centre knot where it cannot be seen, that then outruns it. Three things about it are load-bearing: - **It is a shape, not an opacity.** Fading a full-frame rectangle up to opaque leaves a half-transparent wash lying over the backdrop for several frames, and there is no reading of that except "the screen faded to blue". - **It is keyed to the mark's size, never to the clock.** It cannot begin until the mark is already frame-sized. On a timer it starts while the mark is small, and you get a blue screen with a little logo on it. - **It lags the mark.** Ink is emitted, not carried: the mark punches ahead and its colour is left in the wake. Pin the disc *to* the mark and it flies out to the side with it, which drags the burst off-centre — and then the furthest corner of the frame is the one behind the mark, so the last thing on screen is a colour edge closing backwards, against the direction the whole shot is travelling. **A logo cannot cover the frame by growing**, and that is a shape fact, not a tuning problem: scaling a mark scales its *holes* by exactly as much. Measured on the bundled clover, filling the frame with solid ink by scaling would take **396×**. The ink is what closes them. **The wordmark does not fade out.** The mark blows up straight through it and the ink takes what is left, so the word is *erased* rather than merely absent — and the frame is never empty, which is what fading it used to cost. Set `coverColor` to `"transparent"` to skip the ink and end on the giant mark instead, with the backdrop still showing through it. ### If your logo is an image, it needs `max-width: none` Tailwind's preflight sets `img { max-width: 100% }`, and it applies to `img` but **not** to `svg`. The mark is placed in an absolutely-positioned box at `left: 100%` (the word's right edge), where the width available to an auto-width box is *(containing block − left) = zero*. Shrink-to-fit then asks the content for its minimum width: an `` says "58" and holds the box open, but an image capped at `max-width: 100%` says "I can be zero" — so the box collapses and the mark renders **zero pixels wide**. A blank frame. The component already opts out (`max-width: none` on the mark, `width: max-content` on its box). It is called out here because it will not reproduce in a Remotion render — a bundle has no preflight — so a component can be verified frame by frame in an mp4 and still be invisible in the app that installs it. ### Your own logo Pass `logoSrc` (a URL — use `staticFile()` for your own `public/`), or `logo` (any node; an SVG is better, because it scales forever). It goes through Remotion's ``, so the frame waits for the image instead of rendering a hole. Give it resolution. The rush blows the mark up to about **42×** — a 300px PNG arrives as mush. And set `coverColor` to your mark's colour if it is not `accentColor`, or the ink will not match the mark it is supposed to be coming out of. `before` and `after` are ignored by this preset: it is a lockup, not a sentence. ## Props so the frame waits for it. Use staticFile() for your own public/", }, { name: "logoScale", type: "number", default: "1.05", description: "logo-wipe only: mark size as a multiple of fontSize", }, { name: "spinTurns", type: "number", default: "1", description: "logo-wipe only: turns the mark spins while it wipes the word open", }, { name: "holdDuration", type: "number", default: "12", description: "logo-wipe only: frames the finished lockup rests before the rush", }, { name: "rushDuration", type: "number", default: "24", description: "logo-wipe only: frames the rush at the camera takes. A perspective rush is back-loaded by nature — too few and the growth, the swing and the ink all land in the last three frames", }, { name: "rushScale", type: "number", default: "42", description: "logo-wipe only: the mark's size at the end of the rush, as a multiple of its resting size. Big enough that its own body overruns the frame, which is what lets the ink close the frame with nothing sweeping across it", }, { name: "rushDrift", type: "number", default: "0.55", description: "logo-wipe only: where the swing ends up, as a fraction of the width. Positive is right — across the frame and out past your shoulder. This is the crossing; swingOut is the arc", }, { name: "swingOut", type: "number", default: "0.25", description: "logo-wipe only: about how far the mark bows out to the left on the way, as a fraction of the width. This is what makes the rush a swing rather than a slide — and it is what leaves the mark lying over the left of the frame when its ink goes off, so the colour opens rightward behind it instead of closing backwards", }, { name: "coverColor", type: "string", default: "accentColor", description: "logo-wipe only: the colour the mark's ink floods out in, and the colour the frame ends on. Set it to your mark's own colour if it is not the accent, or \"transparent\" to end on the giant mark instead", }, { name: "springConfig", type: "{ damping?: number; mass?: number; stiffness?: number }", default: "{ damping: 18 }", description: "Spring for the marker and underline sweeps — high damping, no bounce", }, { name: "thickness", type: "number", default: "round(fontSize * 0.08)", description: "Underline/strikethrough line thickness in pixels", }, { name: "shineColor", type: "string", default: "lighter of theme.card / theme.foreground", description: "Shimmer only: color of the sweeping shine", }, { name: "fontSize", type: "number", default: "56", description: "Font size in pixels", }, { name: "fontWeight", type: "number", default: "600", description: "CSS font-weight", }, { name: "speed", type: "number", default: "1", description: "Playback speed multiplier", }, { name: "className", type: "string", description: "Optional className passed to the sentence span", }, { name: "theme", type: "Partial", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Text Reveal URL: https://snapcn.dev/docs/text/text-reveal A cinematic zoom-out title reveal — the lead word floats up close to the viewer, falls back to size, then the sentence pushes in word by word and slides into place *(interactive preview: https://snapcn.dev/docs/text/text-reveal)* The reveal plays in four beats: 1. The lead word fades in large and centred — right up against the viewer. 2. It drifts a little **closer** still, and hangs there. 3. It **falls back** to its final size, roughly in place. 4. Only once it has landed does the line travel left, while the trailing words **push in** one by one from the right and settle. Beats 3 and 4 begin on the same frame but run on very different clocks: the recede is short, the slide is long and heavily eased in. That gap is what makes the word read as going *back* first and moving left second, rather than doing both at once — and the trailing words stay hidden until `wordDelay` frames into the recede, so none of them appear while the lead word is still travelling. Only `scale`, `translate` and `opacity` animate, so the baseline stays fixed and nothing reflows. ## Installation ```bash npx shadcn@latest add @snapcn/text-reveal ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx // src/Root.tsx import { Composition } from "remotion"; import { TextReveal } from "@/components/snap-cn/text-reveal"; const TitleScene = () => ; export const RemotionRoot = () => ( ); ``` Tune the feel with a few props — a bigger `initialScale` for a more dramatic zoom, a longer `assembleDuration` for a slower settle: ```tsx ``` The lead word's centre is measured once on mount so it stays perfectly centred while large and lands exactly in the line — the type, spacing and baseline never shift. `recedeDuration` and `assembleDuration` are the two knobs that shape the reveal. Keep the assemble comfortably longer than the recede (the default 30 vs 14 is a good ratio); shrink the gap and the line starts sliding left while the lead word is still falling back, which reads as one muddled move instead of two. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Text Swap URL: https://snapcn.dev/docs/text/text-swap Replace one line of text with another — by default the old line rushes the camera and flies past you, and the new one is revealed behind it *(interactive preview: https://snapcn.dev/docs/text/text-swap)* ## Installation ```bash npx shadcn@latest add @snapcn/text-swap ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx // src/Root.tsx import { Composition } from "remotion"; import { TextSwap } from "@/components/snap-cn/text-swap"; const TextSwapScene = () => ( ); export const RemotionRoot = () => ( ); ``` Two stacked layers hold the outgoing and incoming lines. The incoming line is scheduled with `enterStart = max(0, exitTotal - overlap + microDelay)`, so the old text is (almost) gone before the new one lands — with a small beat between the two moments. ## Presets The `transition` prop selects one of six looks. Each preset carries its own default `unit`, `exitDuration`, and `enterDuration`; setting those props explicitly overrides the preset. | Preset | Description | | --- | --- | | `fly-through` | **Default.** The old line rushes the camera and passes your face; the new one is revealed behind it, arriving out of depth. | | `fade-through` | Material-style block fade: old line fades out with a small upward drift, new line fades in with a soft blur resolve. | | `crossfade` | Words gently crossfade one after another with a short vertical drift — a calm keynote rhythm. | | `shared-axis-y` | The whole line travels on the vertical axis: exits 24px up, enters from 24px below. | | `shared-axis-z` | Depth swap: the old line scales up and blurs away, the new one settles down from a larger scale. | | `cut` | Per-word hard cuts with stepped staircase timing for sharp editorial swaps. | ## How `fly-through` works, and why it is not just a big scale Three things, and each of them is load-bearing. **Perspective, not easing.** The line does not grow on a ramp — it *travels toward the eye*, and its apparent size goes as `1 / (1 - travel)`. So it creeps for most of the trip and then blows up right at the end, which is the whole character of something rushing past you. This cannot be faked with an easing curve. Fitted against the reference recording, a plain `interpolate` to a big scale misses by rmse 0.31 and can never reach the blowup at all; the perspective model lands at 10.09x where the reference measured 10.28x. The easing describes the *travel*, which is nearly linear — the drama is geometry, not curve-shaping. **A shutter, not a blur filter.** At the end of the rush the line more than doubles between one frame and the next. Drawn sharp, that does not read as speed — it reads as strobing. So the exit is sampled several times across the frame and the samples are averaged, which is exactly what a camera shutter does. Because the motion is a scale, the samples fan out radially: sharp at the centre of the rush, streaked at the edges. That radial smear is the thing that makes it feel fast, and you cannot get it from `filter: blur()`, which softens everything equally. **It stays solid until it is gone.** A line flying at your face does not dim on the way in. `exitFadeStart` holds the fade back until 72% of the travel has passed, and then it goes. The cost is real: the shutter draws the outgoing line 18 times per frame. That is free in a render and expensive in a live browser preview, which is why this component ships a **pre-rendered demo** on the docs site. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Text Swell URL: https://snapcn.dev/docs/text/text-swell The lead word rises into the middle and floats toward you — then the rest of the sentence pushes in from the right, bouncing its letters, and shoves the lead word aside before the whole line falls back *(interactive preview: https://snapcn.dev/docs/text/text-swell)* A title reveal built around one idea: **the lead word is pushed aside by the words that follow it.** 1. The lead word **rises from below** and settles, centred, at its natural size. 2. The line **floats forward** — toward the viewer. The lead word is still alone, so it stays dead centre as it grows. 3. The second word cuts in from the right, its **letters bouncing** up off the baseline one after another, and **shoves the lead word left** to make room. Later words follow, pushing in **without a bounce**, each shoving the lead word further left. 4. Once everything has landed, the whole line **falls back** to its final size and settles into the sentence. ## The shove The shove is the whole point, so it is not on a clock of its own — that would just be a word arriving and a line sliding at the same time, and it would read as two things happening rather than one thing causing another. Instead, each trailing word **owns a share of the leftward travel** — its share of the width it adds to the line — and spends that share over exactly the frames it spends pushing into its slot. The lead word therefore cannot move until a word arrives to move it, and once every word has landed it sits exactly where the finished sentence needs it. Nothing is scheduled twice, so nothing can drift out of sync. ## The bounce `bounceWords` controls how many trailing words bounce their letters in. The default is `1`: only the second word bounces, and every word after it simply pushes in. The lead word never bounces. Each letter swells up **off its baseline** — not its centre — so the word grows upward and stays planted on the line. Both halves of the pulse use a curve that starts and ends at a standstill, so a letter never jerks off the baseline and the top of the bounce rounds over instead of cornering. `letterStagger` is the wave's speed; `letterHold` adds a flat top (leave it at `0` for a clean quick pop). ## Installation ```bash npx shadcn@latest add @snapcn/text-swell ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx // src/Root.tsx import { Composition } from "remotion"; import { TextSwell } from "@/components/snap-cn/text-swell"; const TitleScene = () => ; export const RemotionRoot = () => ( ); ``` ## Why scaled text looks "stuck", and what actually fixes it If you have ever animated `scale` on a heading and watched the letters judder and stick instead of gliding, this is why — and it is worth knowing, because the obvious fixes are mostly wrong. **The cause is the type, not the animation.** Browsers do not scale text the way they scale an image. They re-shape and re-rasterise the glyphs at every new size, and the rasteriser snaps each glyph's origin to the pixel grid — in Skia, horizontal positions quantise to ¼px and **vertical positions have no sub-pixel precision at all**, they round to a whole pixel. So under a perfectly smooth scale ramp the letters sit on one pixel row for a few frames, snap to the next, and sit again. Measured on a linear 1.6× → 1× ramp, the line's vertical centroid reversed direction 29 times in 40 frames while its true motion was a straight line. **The fix is to lay the line out at the biggest size it will ever be shown at, and only ever scale it _down_** (`fontSize: fontSize * raster`, `scale: scale / raster`), with `will-change: transform` so the line is composited. Compositing hands the scale to the GPU, which resamples a texture instead of re-shaping type — the snapping cannot happen. Laying out big is what makes that safe: the transform never asks for more detail than the texture already holds, so the browser rasterises once and never revisits it. Same measurement after the change: **0.03px of quantisation instead of 0.30px**, and no loss of sharpness, because the texture was rasterised at the largest size in the first place. `will-change: transform` **on its own is not the fix** — scaling up past the texture's resolution forces the browser to re-rasterise mid-animation, which trades the jitter for blur and pops. The two halves only work together. Not fixes, despite being widely repeated: `translateZ(0)`, `backface-visibility: hidden`, `perspective(1px)`. They are all layer-promotion hacks with the same blur side effect. And `-webkit-font-smoothing: antialiased` does not affect glyph positioning at all. **One more, unrelated to rasterisation: no asymptotic easing on a frame clock.** Aggressive decelerating curves — quint-out, expo-out — cover 99% of their travel in the first third and then crawl. Over a 50px rise at 30fps that leaves five frames moving less than half a pixel each, which rasterise to *identical frames*: the word visibly stops dead partway and waits. Every curve here still arrives at a standstill, but none of them spend more than a frame or so on travel you cannot see. A settle worth one frame is a settle; a settle worth five frames is a freeze. ## frontScale is a ceiling, not a promise Floating forward blows the line up about its left edge, and until the last word has finished shoving, that edge still sits right of where it will rest. A word arriving into that gap is the widest the line ever gets — so `frontScale` is capped at whatever keeps even that moment inside the frame. That means a long sentence floats less far forward than you asked for. It is not a bug you can tune around; it is the only way to have every word on screen at once while the line is forward. If you want the full, dramatic push, **use fewer or shorter words** — the cap lifts as the line gets narrower. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Type Morph URL: https://snapcn.dev/docs/text/type-morph A headline that types itself under a glowing caret, sheds its lead, morphs letter by letter, and ends under a colour flood *(interactive preview: https://snapcn.dev/docs/text/type-morph)* ## Overview Four mechanics on one timeline. **Typing.** The line is centre-anchored, so it grows in both directions rather than running to the right. A glyph lands in the accent colour and cools to ink over the next two glyphs — that moving warm edge is what reads as typing, more than the caret does. **The cut.** The lead is removed in a single frame. It is not animated out; what *is* animated is the slide back to centre that follows it. **The morph.** Glyphs shared between the two phrases travel from where they were to where they belong, and the rest cross-fade in place. Every position is measured in the real face at the real size behind a hidden probe, because a morph built on estimated advance widths lands each letter a pixel or two off and reads as a wobble. **The exit and the flood.** The line does not hold still and then cut — it slides left for six frames first, accelerating, and the cut lands on a line that was already leaving. Then two flat colours, four frames each, not a ramp: fading a full-frame rectangle up leaves a half-transparent wash over the type for several frames, and there is no reading of that except the screen dimming. `wordOnFlood` is off by default, so the flood ends clean. ## Notes `accent` defaults to the theme's accent, and `flood` to a violet pair. Both take your own colours; `flood={false}` ends the beat on paper instead. `background` defaults to the theme's page. The reference's is a plain near-white rather than the warm off-white the token carries, so the preview passes its own. There is no glow behind the caret — just the bar. An earlier version had one, and it is worth recording why it was hard to get right: it was built with `mixOklch` toward the page, and `mixOklch` walks the *hue* path. From a violet accent to a warm off-white that path goes through pink, so the halo rendered salmon. If you add a glow back, composite it with `withAlpha` over the page — alpha has no hue path. --- # Word Flip URL: https://snapcn.dev/docs/text/word-flip A headline types itself out, then one word cycles on a 3D flip — sinking into an anticipation dip, throwing up and away about its baseline under motion blur, while the next word unfurls from below. *(interactive preview: https://snapcn.dev/docs/text/word-flip)* ## Installation ```bash npx shadcn@latest add @snapcn/word-flip ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx import { WordFlip } from "@/components/snap-cn/word-flip"; export function Hero() { return ( ); } ``` ## How it moves The sentence types itself a character at a time, holds for a beat, and then the word in the middle starts cycling. Four things happen in the nine frames of a flip, and they are all one gesture. ### The slot is reserved before the first word exists The whole point of this animation is that **nothing except the word ever moves** — no horizontal shift, no baseline shift, no reflow. That cannot be achieved by swapping text in and out of a line, because the words have different widths and the line would breathe every time one arrived. So the slot is an `inline-block` whose width is the widest word's, fixed from the first frame. While the sentence is still typing, the slot is simply *empty* — the gap you see between "A" and "Portfolio" is the word's seat, waiting for it. Every word is then uniformly scaled to fill that seat, which is why the widest word sits at scale 1 and the rest are scaled up to meet it, and why both edges of the slot are pinned no matter which word is showing. Measured across the whole clip: the left edge of "Portfolio" moves **1px**, and that 1px is the antialiasing on its own fade-in. ### The word sinks before it flies The flip does not start by lifting the word. It starts by dropping it about a tenth of an em — a backswing, the same thing a person does with their arm before they throw. It is what makes the flip read as weight instead of as a cut. That is not a separate keyframe. One `easeInBack` progress drives the translate *and* the rotation, and the curve's own overshoot is the dip: its minimum is `p = -0.100` at `t = 0.42`, so the dip is worth exactly a tenth of the exit travel and lands a little under halfway through it. ### The blur is a shutter, not a ramp Blur is proportional to **speed** — `easeInBackSpeed` is the analytic derivative of the curve above. It matters because the derivative is *zero at the turnaround*: at the bottom of the dip the word has stopped to change direction, and it is sharp there. Three frames later it is travelling as fast as it ever will, and it is smeared. A blur keyed to progress instead of speed would blur the word while it is standing still, which reads as a focus pull rather than motion. ### The two words overlap The incoming word starts before the outgoing one is gone (`overlap`, in frames), so the swap is continuous rather than sequential. It arrives rotated `-90°` about its baseline, a touch below where it belongs and a touch small, and unfurls into place on a moderate decelerate — never an expo-out, which on a frame clock spends most of its frames moving less than a pixel and reads as the word stopping dead partway up and waiting. Everything pivots on the **baseline**, and the baseline is measured, never derived from a line-height ratio. Pivot anywhere else and the glyph origins climb the pixel grid in whole-pixel jumps as the scale moves them — the word sits still, jumps, sits still. ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `prefix` | `string` | `"Looking For A"` | Text before the flipping word. | | `words` | `string[]` | `["Modern", "Stunning", "Minimal"]` | The words that cycle through the slot. | | `suffix` | `string` | `"Portfolio"` | Text after the flipping word. | | `gradient` | `string[]` | `["#2A8BF5", "#D382B0"]` | Gradient painted on the flipping word, left to right. | | `cps` | `number` | `9` | Typing speed, characters per second. | | `typeStart` | `number` | `4` | Frames before the first keystroke. | | `charFade` | `number` | `6` | Frames each character takes to fade in. | | `jitter` | `number` | `0.18` | Keystroke unevenness, as a fraction of one keystroke. `0` is a metronome. | | `pause` | `number` | `6` | Frames held after the sentence completes, before the first flip. 6 frames is the 200ms beat. | | `cycle` | `number` | `35` | Frames from one flip to the next. | | `exitDuration` | `number` | `9` | Frames the outgoing word takes to leave. | | `enterDuration` | `number` | `9` | Frames the incoming word takes to settle. | | `overlap` | `number` | `3` | Frames the incoming word starts before the outgoing one is gone. | | `caret` | `boolean` | `true` | Blinking caret while typing. | | `loop` | `boolean` | `true` | Keep cycling the words forever. | | `motion` | `Partial` | see below | Distances, in em. | | `perspective` | `number` | `6.5` | 3D depth, in em. Smaller is a stronger perspective. | | `fontFamily` | `string` | app sans stack | The typeface. | | `fontSize` | `number` | `72` | | | `color` | `string` | `theme.foreground` | The static part of the sentence. | | `theme` | `Partial` | — | Design-system token overrides. | | `mode` | `"light" \| "dark"` | `"light"` | Which end of the system to resolve against. | | `fontWeight` | `number` | `600` | | | `speed` | `number` | `1` | | ### `motion` Every distance is in **em**, so the gesture scales with the type instead of being tied to one font size. | Key | Default | Description | | --- | --- | --- | | `exitY` | `-1.29` | How far the outgoing word is thrown. Negative is up. Its first tenth, inverted, *is* the dip. | | `enterY` | `0.135` | How far below rest the incoming word starts. | | `rotate` | `90` | Degrees about the baseline. | | `scale` | `0.98` | What the outgoing word shrinks to, and the incoming grows from. | | `blur` | `0.085` | Peak blur, reached at peak speed. | ## Notes - The sentence is one line and never wraps. `fontSize` is the knob: at the default 72 the reference sentence fills about 83% of a 1280-wide composition. - `caret` has no effect on layout — it is a zero-width box with an absolutely positioned bar inside, so the sentence's geometry is identical with it and without it. - The words are measured once, behind `delayRender()`, so frame 0 is never captured against the wrong slot width. --- # Captions URL: https://snapcn.dev/docs/captions Burned-in, transcript-timed subtitles with word-pop and karaoke styles and safe-area presets for 16:9, 1:1 and 9:16. Use captions when someone is speaking and the words need to be on screen — talking-head clips, voiceover demos, anything watched muted on a feed. These components take a timed transcript and render frame-accurate, burned-in subtitles inside per-aspect safe areas. For decorative headlines and titles, use [Text & Titles](/docs/text) instead. --- # Karaoke Captions URL: https://snapcn.dev/docs/captions/karaoke-captions Full-line captions with a progressive fill sweep across the spoken words and accent-colored keyword emphasis *(interactive preview: https://snapcn.dev/docs/captions/karaoke-captions)* ## Installation ```bash npx shadcn@latest add @snapcn/karaoke-captions ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage The whole line stays on screen while a color fill sweeps left-to-right across the words to indicate speech progress — muted `#667085` ramps to ink `#101828` in the light theme, and a translucent-white ramp resolves to `#FAFAFA` in the dark theme. Emphasized words fill in the accent color and scale up subtly. Everything is driven by the current frame, so scrubbing and rendering are fully deterministic. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { KaraokeCaptions } from "@/components/snap-cn/karaoke-captions"; const CaptionScene = () => ( ); export const RemotionRoot = () => ( ); ``` Without `lines`, the component builds a single demo line from `text` and `emphasize`. For word-accurate karaoke timing (e.g. from a transcription API), pass `wordTimings` — the absolute frame each word starts filling; a word's fill ends when the next word begins, and the last word runs to `endFrame`: ```tsx ``` The component renders with a transparent canvas (plus the optional pill surface), so place it over any footage or backdrop. Unlike word-by-word caption styles, the full line is always visible — the fill is what indicates progress. ### Safe-area presets The `aspect` prop positions the caption block clear of platform UI: - `landscape` — 16:9 videos; sits 8% from the bottom with 10% side margins. - `portrait` — 9:16 (TikTok / Reels / Shorts); raised to 18% from the bottom to clear platform chrome, 7% side margins. - `square` — 1:1 feeds; 10% from the bottom, 8% side margins. ## Using a real transcript Nobody has frame numbers. Your transcript is in **milliseconds** — the shape Whisper, CapCut and Remotion's own `@remotion/captions` all speak: ```tsx ``` or paste an `.srt` straight in with `srt={...}`. Word-level output is collapsed into LINES (consecutive words become one line, a pause starts a new one) — which is exactly how a caption tool builds a line. Milliseconds are converted against `useVideoConfig().fps`, so one transcript is correct at any frame rate. ## What actually makes a caption look premium Three things, and the first one is most of it. ### 1. An OUTSIDE outline A caption has to stay legible over footage it has never seen — a face, a sky, a white desk. A heavy black outline is the only thing that does that, and it has to sit *outside* the letterform. `-webkit-text-stroke` **centres** the stroke on the glyph outline, so half of it eats inwards and the letters go thin and mushy. Measured on Montserrat 900 at 160px with a 14px stroke: | | white stem | | --- | --- | | no stroke | 38px | | `-webkit-text-stroke` alone | **22px** — the stroke eats 42% of the letterform | | `+ paint-order: stroke fill` | **38px** — identical to no stroke | ```tsx WebkitTextStrokeWidth: `${stroke}px`, WebkitTextStrokeColor: "#000", paintOrder: "stroke fill", // draw the stroke, THEN the fill over it ``` That one line is most of the difference between a caption and a cheap caption. ### 2. Weight and size Montserrat 800–900, at **11–13% of the frame's short side**. Not Inter at 700, and not 2.8% of the height — that is a subtitle. Load the face through `@remotion/google-fonts` so the Player, the mp4 and your own project all get the same one. ### 3. The spoken word lands A spring with overshoot, not an ease. And that scale is a scale **on text**, which is where captions usually fall apart: a browser gives glyph origins no vertical sub-pixel precision, so a scale that moves the baseline makes the word climb the pixel grid in whole-pixel jumps. The pop pivots on the **measured baseline** — verified on rendered frames, the baseline holds to **0px** while the word scales 1.16×, so it grows upward off its baseline exactly like real caption type. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "accentColor", type: "string", default: "theme.primary", description: "Fill color of emphasized words", }, { name: "fontSize", type: "number", default: "48", description: "Font size in pixels", }, { name: "fontWeight", type: "number", default: "600", description: "CSS font-weight", }, { name: "aspect", type: "\"landscape\" | \"portrait\" | \"square\"", default: "\"landscape\"", description: "Safe-area preset positioning the caption block", }, { name: "pill", type: "boolean", default: "true", description: "Rounded surface behind the line with a hairline border", }, { name: "emphasisScale", type: "number", default: "1.04", description: "Scale emphasized words grow to as their fill completes (ease-out, no bounce)", }, { name: "baseColor", type: "string", description: "Unfilled (not-yet-spoken) word color override", }, { name: "fillColor", type: "string", description: "Filled (spoken) word color override", }, { name: "speed", type: "number", default: "1", description: "Global playback multiplier", }, { name: "className", type: "string", description: "Optional className passed to the outer wrapper", }, ]} /> --- # Word Captions URL: https://snapcn.dev/docs/captions/word-captions TikTok-style word-by-word burned-in captions from a timed transcript *(interactive preview: https://snapcn.dev/docs/captions/word-captions)* ## Installation ```bash npx shadcn@latest add @snapcn/word-captions ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage Feed it a timed transcript — one `{ text, startFrame, endFrame? }` per word, e.g. mapped from Whisper word timestamps — and it burns captions into the frame: the current beat sits in a rounded backdrop pill inside the safe area of your aspect, and each word pops the moment it is spoken. The component has a transparent background, so overlay it on your screen recording or any scene. ```tsx // src/Root.tsx import { AbsoluteFill, Composition, OffthreadVideo, staticFile } from "remotion"; import { WordCaptions } from "@/components/snap-cn/word-captions"; const CaptionedClip = () => ( ); export const RemotionRoot = () => ( ); ``` A word's `endFrame` defaults to the next word's `startFrame`, so contiguous transcripts only need start times. For quick drafts you can also pass `words` as a plain string — it is split on whitespace and paced evenly at `framesPerWord`. Set `groupSize={2}` or `{3}` to show short phrases per beat instead of single words. ## Active-word styles - `pop` — the spoken word scales to 1.06 with a fast 6-frame high-damping spring (no bounce). - `highlight` — the spoken word gets a rounded accent-colored chip behind it (CapCut-style). - `color` — the spoken word's text switches to the accent color; the rest stays white. ## Aspect presets - `9:16` — captions ride 20% up from the bottom, clear of feed UI (like/comment/share chrome). - `1:1` — captions sit 11% up from the bottom edge. - `16:9` — classic lower-third placement, 8% up from the bottom. ## Using a real transcript This is the part that matters. Nobody has frame numbers — you have a transcript, and it is in **milliseconds**. Whisper, whisper.cpp, CapCut, Submagic, Opus Clip, Descript and Remotion's own `@remotion/captions` all speak the same shape: ```ts type Caption = { text: string; startMs: number; endMs: number }; ``` So that is what this takes. Drop your Whisper output straight in: ```tsx import { WordCaptions } from "@/components/snap-cn/word-captions"; // whatever your transcription tool gave you — word level or phrase level const captions = [ { text: "You", startMs: 1000, endMs: 1180 }, { text: "are", startMs: 1180, endMs: 1320 }, { text: "losing", startMs: 1320, endMs: 1700 }, { text: "three", startMs: 1700, endMs: 2050 }, { text: "hours", startMs: 2050, endMs: 2400 }, ]; ; ``` Or paste an `.srt` in as-is — cues get split into words for you, so the active-word highlight still tracks: ```tsx ``` The frame maths is the component's problem, not yours: milliseconds are converted against `useVideoConfig().fps`, so the same transcript is correct at 24, 30 or 60fps. ### Pages, not chunks Captions are grouped into **pages** the way real caption tools group them — by the shape of the speech. A page ends when it runs out of words, runs out of its character budget, or **the speaker pauses** (`pageBreakMs`). Chopping every N words regardless is how you end up with `STOP / LOSING / HOURS` stacked in a tower: three words that never belonged on a page together. `maxWords`, `maxChars` and `pageBreakMs` override the preset if you need to. ## What actually makes a caption look premium Three things, and the first one is most of it. ### 1. An OUTSIDE outline A caption has to stay legible over footage it has never seen — a face, a sky, a white desk. A heavy black outline is the only thing that does that, and it has to sit *outside* the letterform. `-webkit-text-stroke` **centres** the stroke on the glyph outline, so half of it eats inwards and the letters go thin and mushy. Measured on Montserrat 900 at 160px with a 14px stroke: | | white stem | | --- | --- | | no stroke | 38px | | `-webkit-text-stroke` alone | **22px** — the stroke eats 42% of the letterform | | `+ paint-order: stroke fill` | **38px** — identical to no stroke | ```tsx WebkitTextStrokeWidth: `${stroke}px`, WebkitTextStrokeColor: "#000", paintOrder: "stroke fill", // draw the stroke, THEN the fill over it ``` That one line is most of the difference between a caption and a cheap caption. ### 2. Weight and size Montserrat 800–900, at **11–13% of the frame's short side**. Not Inter at 700, and not 2.8% of the height — that is a subtitle. Load the face through `@remotion/google-fonts` so the Player, the mp4 and your own project all get the same one. ### 3. The spoken word lands A spring with overshoot, not an ease. And that scale is a scale **on text**, which is where captions usually fall apart: a browser gives glyph origins no vertical sub-pixel precision, so a scale that moves the baseline makes the word climb the pixel grid in whole-pixel jumps. The pop pivots on the **measured baseline** — verified on rendered frames, the baseline holds to **0px** while the word scales 1.16×, so it grows upward off its baseline exactly like real caption type. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Logos URL: https://snapcn.dev/docs/logos Logo stings and entrances for brand moments in launch videos. Reach for this category at the brand beats of a video: the opening sting, or an integrations moment mid-demo. Each component takes your mark (SVG or wordmark) and handles the entrance choreography and hold. --- # Block Wordmark URL: https://snapcn.dev/docs/logos/block-wordmark A wordmark that builds itself out of solid blocks — a deck shuffles, collapses, splits one block per letter, and each block flashes and cuts to its real letterform *(interactive preview: https://snapcn.dev/docs/logos/block-wordmark)* ## Installation ```bash npx shadcn@latest add @snapcn/block-wordmark ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage A wordmark build in six beats: 1. **Seed** — a single rounded square scales up off the baseline (`growDuration`). 2. **Shuffle** — coloured cards fan out from behind it and wind back in, one full turn each about a *shared* pivot on the baseline. The fan is one S-curve sampled at `cardStagger` offsets, so it opens and closes exactly once (`cards`, `spinTurns`, `spinDuration`, `liftRatio`). 3. **Collapse** — the deck lands back on the seed square and dips once, which is the only frame in the piece where the ink is a single block. 4. **Split** — the square stretches horizontally and splits into one block per letter, outermost blocks leading and the inner ones lagging by `splitStagger`. Any letter whose ink rises above the x-height grows a tab on the correct side — the "b" of `base` sprouts a left stem, a "d" a right one, an "A" an apex. 5. **Hold** — the block row sits and scales up slowly for `holdDuration` frames. This is the beat that reads as a logo rather than as an animation. 6. **Resolve** — each block flashes a colour from `colors`, returns to ink, and is **hard-cut** to its letterform `flashToSwap` frames later. The first letter goes last, so the word finishes where it started. Nothing crossfades and nothing morphs. At each swap frame the block is already within a few pixels of the settled letter, so the cut is invisible — and a spring or a crossfade "to improve it" is the one change that breaks the effect. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { BlockWordmark } from "@/components/snap-cn/block-wordmark"; const Sting = () => ( ); export const RemotionRoot = () => ( ); ``` ### Any word, not just this one Nothing about the geometry is hardcoded to a four-letter lowercase word. Every glyph's advance width, x-height and ink top are measured at runtime behind `delayRender()`, so the blocks are sized to the letters they become and the ascender tabs are found by measurement rather than a letter list. Emoji survive (the text is split by code point) and spaces get a gap, not a block. Two knobs decide how literal the morph is: - `blockSizing="square"` (default) makes every block a square the height of the x-height, so the row reads as an even rhythm and each block visibly *changes* into its letter. - `blockSizing="glyph"` sizes each block to its letter's advance width, which makes the resolve nearly in-place. Long words outrun the 150-frame default: the last letter resolves at roughly `94 + flashStagger × letters`. Drop `flashStagger` to 2 or raise `durationInFrames`. ### Colour The deck and the flash colours are a prop with our defaults — the first card follows your `primary` token, the rest are a decorative set. The **ink** defaults to your `foreground`, so an installed copy paints in whatever palette it lands in. The preview above passes `color="#0000FF"` because that is snapcn's own mark; yours is one prop away. A full brand look — ink, deck, and a display face's heavier ascender — is one line. `base` rather than `snapcn` here because the ascender props only show on a word that has one: ```tsx ``` `ascenderRatio` and `ascenderStemRatio` are the two places a display face differs most from the face in use: leave them out and they are measured per glyph, set them and you get the chunky flag a geometric sans draws. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Logo Assemble URL: https://snapcn.dev/docs/logos/logo-assemble A scatter of cards collapses into a brand card that is born tilted and settles — revealing the logo and brand name *(interactive preview: https://snapcn.dev/docs/logos/logo-assemble)* ## Installation ```bash npx shadcn@latest add @snapcn/logo-assemble ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage A logo build in three beats: 1. **Orbit** — a single ring of image cards revolves around the centre, holding a line of `middleText` in the clear middle. 2. **Collapse** — the ring spins inward and drains to the centre, the cards vanishing. 3. **Reveal** — a simple, monochrome logo is born at the centre, then slides left, and the brand name reveals to its right. Pass your `logoSrc` (kept simple/monochrome), your `brandName`, the `middleText`, and the `images` that ride the ring. Everything is frame-driven, so scrubbing and rendering are deterministic. ```tsx // src/Root.tsx import { Composition, staticFile } from "remotion"; import { LogoAssemble } from "@/components/snap-cn/logo-assemble"; const Sting = () => ( ); export const RemotionRoot = () => ( ); ``` Assets take a root-relative `/…` path (served by Next in the Player, rewritten through `staticFile()` in a render) or an `http(s)`/data URL. This is a fast collapse and a scale-on-imagery landing, which a live `` mispaces on a high-refresh display — so the docs preview plays the **rendered mp4**. Re-render after any change: `pnpm run render:previews --only logo-assemble`. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Logo Flicker URL: https://snapcn.dev/docs/logos/logo-flicker Images flip across the screen very fast, the flicker decelerates and fades, and the logo and brand name resolve *(interactive preview: https://snapcn.dev/docs/logos/logo-flicker)* ## Installation ```bash npx shadcn@latest add @snapcn/logo-flicker ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage A Marvel-Studios-style flicker reveal: 1. **Flicker** — the `images` flip **full-screen** at a **constant** fast rate (one image per `flipInterval` frames), right to the end — no slow-down. 2. **Cross-fade** — the flicker fades **out** while the logo and brand name fade **in**, on the exact same window — one move, not one after the other. Pass your `logoSrc` (simple/monochrome) and `brandName`, and the `images` the flicker flips through. Everything is frame-driven, so scrubbing and rendering are deterministic. ```tsx // src/Root.tsx import { Composition, staticFile } from "remotion"; import { LogoFlicker } from "@/components/snap-cn/logo-flicker"; const Sting = () => ( ); export const RemotionRoot = () => ( ); ``` The flicker swaps the full-screen image every couple of frames, which a live `` shows as harsh flashing before the browser has the images cached — so the docs preview plays the **rendered mp4**. Re-render after any change: `pnpm run render:previews --only logo-flicker`. Note: this is a fast full-screen image strobe. Raise `flipInterval` (slower flips) if you need it gentler for photosensitivity. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Screens & Devices URL: https://snapcn.dev/docs/screens Phone frames, terminals and AI-product simulations — the demo toolkit. This is the category for showing product. Wrap a recording or live UI in device chrome, or simulate a terminal or AI-tool session entirely in code. Reach for it in the demo section of a launch video — anywhere a screen is the subject of the shot. --- # Laptop Frame URL: https://snapcn.dev/docs/screens/laptop-frame MacBook-style laptop that opens, runs a notch notification, then dives into the screen until it fills the frame *(interactive preview: https://snapcn.dev/docs/screens/laptop-frame)* ## Installation ```bash npx shadcn@latest add @snapcn/laptop-frame ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage The lid **opens**, the notch runs its idle → connecting → connected notification, and then the camera **dives into the screen** — un-tilting it and scaling it up until the content fills the frame. Drop in a screenshot, a screen recording, or your own UI. Everything is driven by the current frame, so scrubbing and rendering are fully deterministic. The component ships with a transparent background; supply the backdrop from the scene. ```tsx // src/Root.tsx import { Composition, staticFile } from "remotion"; import { LaptopFrame } from "@/components/snap-cn/laptop-frame"; const ProductScene = () => ( ); export const RemotionRoot = () => ( ); ``` `screenSrc` takes an **image or a video** — a `.mp4/.webm/.mov/.m4v` plays through ``, anything else through ``, both covering the screen and fading in. Pass a `staticFile()` path (or a root-relative `/…` one) for a bundled asset, or an `http(s)`/data URL. For full control, pass `children` instead. Omit both and you get a built-in product-hero placeholder. ### Entrances - `rise` — fades in while translating up 70px with a fast ease-out; the default launch look. - `open` — the lid swings up from the deck (82° → rest tilt) as the machine fades in. - `none` — appears already settled, for scenes that composite the laptop in some other way. ### The screen takeover (`finale`) `finale="zoom-to-screen"` is the payoff: from frame 140 the camera dollies into the screen and flattens the lid to head-on (rotateX → 0), so by frame 185 the screen content **covers the whole frame** and holds. The cover scale is derived from the composition size, so it lands filling the frame exactly — this assumes the laptop occupies the full composition. Set `finale="none"` to just leave the laptop sitting. This is a slow scale on the whole screen over many frames, which a live `` mispaces on a high-refresh display — so the docs preview here plays the **rendered mp4**. Re-render it after any change: `pnpm run render:previews --only laptop-frame`. ### The notch The notch is a dynamic-island-style pill that morphs through three states: an idle camera dot, three connecting dots, then the connected label with a battery. It lands "connected" at frame 120 (then fades as the dive begins) — set `showNotch={false}` for a plain lid, or change `notchLabel` and `batteryLevel` to suit the scene. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Phone Frame URL: https://snapcn.dev/docs/screens/phone-frame iPhone-style device frame with a dynamic island and a screen slot — flat, tilted, or a cinematic 3D showcase crane move *(interactive preview: https://snapcn.dev/docs/screens/phone-frame)* ## Installation ```bash npx shadcn@latest add @snapcn/phone-frame ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage Drop any screen content into the frame — an app UI, an `` screen recording, or an `` — and the device rises in over your backdrop, then keeps a subtle float loop. Everything is driven by the current frame, so scrubbing and rendering are fully deterministic. The component ships with a transparent background; supply the backdrop from the scene (an ambient gradient works well). ```tsx // src/Root.tsx import { Composition, staticFile } from "remotion"; import { PhoneFrame } from "@/components/snap-cn/phone-frame"; const AppLaunchScene = () => ( ); export const RemotionRoot = () => ( ); ``` `screenSrc` takes an **image or a video** — a `.mp4/.webm/.mov/.m4v` plays through `` (muted), anything else renders through ``, both cover-fit to the screen. Omit both `children` and `screenSrc` to get the built-in ride-summary placeholder — a dark map whose glowing GPS route draws itself while stat "insight" pills pop in along it. On light scenes, keep the silver `bezelColor`; on dark ones, pass `mode="dark"` and the shell follows the theme. Unlike a desktop browser window, this is a portrait device with a real dynamic-island cutout and a 3D tilt (`tilt`) or cinematic crane move (`showcase`) for hero shots. ### Variants - `flat` — faces the camera head-on. - `tilt` — a static perspective `rotateY` (see `tiltAngle`). - `showcase` — a cinematic crane move: the camera starts low at the bottom-left corner of the laid-back device, sweeps up the screen while zooming in, then pulls back and settles on a straight frontal view. Normalized to the composition length, so it always completes. ### Entrances - `rise` — fades in while translating up 70px with a fast ease-out; the default launch look. - `rotate-in` — rises with a slight -8° in-plane rotation and a 0.94 → 1 scale settle. - `float` — no travel: fades and grows from 0.96 in place, with the float loop running from frame 0. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Terminal Simulator URL: https://snapcn.dev/docs/screens/terminal-simulator Single-camera flythrough — one continuous camera whips from a headline to a package-manager command panel to a zoomed green terminal, all on one canvas *(interactive preview: https://snapcn.dev/docs/screens/terminal-simulator)* ## Installation ```bash npx shadcn@latest add @snapcn/terminal-simulator ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage Everything lives on **one black canvas** and a **single camera** flies over it — nothing slides in from a screen edge. The camera holds on the **intro headline**, creeps left, then **suddenly accelerates** across to the **command panel** (package-manager tabs + a syntax-coloured install command with the package name highlighted), pushes in slowly while it types — leaving the trailing motion-blur copies — then pulls back, descends, and **punches in** on the green **focus terminal** as it types its output. Fast camera moves draw a velocity-driven ghost trail; holds are dead still. Output reveals in bursts (chunked, not char-by-char), and any line ending in `...` freezes the frame for 18 frames — or set `pause` explicitly. Everything is driven by the current frame, so scrubbing and rendering are fully deterministic. Pass `intro={null}` or `command={null}` to skip a station — the camera path closes the gap automatically. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { TerminalSimulator } from "@/components/snap-cn/terminal-simulator"; const InstallScene = () => ( ); export const RemotionRoot = () => ( ); ``` ### Intro headline `intro` is a single line rendered in the sans stack, centered. Wrap words in `*asterisks*` to paint them with the accent gradient (blue → violet); everything else stays white. Set `intro={null}` to skip straight to the command panel. ### Focus terminal lines The final station renders each line large and zoomed, right of the green accent bar. `command` lines are white and carry the blinking accent cursor (the last one keeps it after it finishes typing); `log` lines are dim, `success` lines take the accent green, `error` lines red. `zoom` sets the landing zoom of the final punch (1.5x by default, 2.4x with `zoom` on). ## Props ", description: "The focus-terminal output (final station). `delay` is the frame gap before a line starts typing (default 8). `pause` freezes the frame after the line finishes; lines ending in \"...\" auto-freeze for 18 frames when `pause` is omitted.", }, { name: "intro", type: "string | null", default: "\"*work* one step at a *time.*\"", description: "The first camera station. Wrap words in *asterisks* to tint them with the accent gradient. `null` or \"\" skips the station.", }, { name: "command", type: "{ managers?: string[]; text: string } | null", default: "{ managers: ['npm','pnpm','yarn','bun'], text: 'npm install One Tool' }", description: "The second camera station. `managers` are the tab labels (first is active); `text` is typed into the row — manager and subcommand dim, the package name highlighted. `null` skips the station.", }, { name: "background", type: "string", default: "theme.card", description: "Command-panel surface color", }, { name: "borderColor", type: "string", default: "theme.border", description: "Hairline color for the command-panel ring and divider", }, { name: "fontSize", type: "number", default: "18", description: "Base monospace size. The command panel and focus terminal scale up from it", }, { name: "charsPerFrame", type: "number", default: "2", description: "Reveal speed: how often the chunked reveal steps (a step every 1 / charsPerFrame frames)", }, { name: "chunkSize", type: "number", default: "3", description: "Characters revealed per step — larger chunks make output lurch in bursts like real command output", }, { name: "zoom", type: "boolean | { enabled: boolean; scale?: number; followCursor?: boolean }", default: "false", description: "Landing zoom of the final punch onto the terminal. Off = 1.5x; `true` or `{ scale }` overrides (default 2.4)", }, { name: "speed", type: "number", default: "1", description: "Global playback multiplier applied to the whole session", }, { name: "className", type: "string", description: "Optional className passed to the outer container", }, { name: "theme", type: "Partial", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Social Proof URL: https://snapcn.dev/docs/social Follower counts, notifications and community milestones — the beat of a video that says other people are already here. Reach for this category when the point of the shot is that other people showed up: the follower count climbing, the notifications stacking, the wall of faces. It is the beat that sits between the demo and the call to action — and the one that is hardest to fake convincingly with a screen recording. --- # Follower Rush URL: https://snapcn.dev/docs/social/follower-rush An X-style follower notification that piles up — avatars stack in and the count explodes, then the row bends into an undulating wave of faces *(interactive preview: https://snapcn.dev/docs/social/follower-rush)* ## Installation ```bash npx shadcn@latest add @snapcn/follower-rush ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage Built for the "we blew up" milestone beat. It opens as a single X-style notification — `Manon ✓ followed you` — then reorganizes into a stacked pile as more followers pour in: avatars pop on one by one and the count climbs slowly, then detonates from a couple dozen to the target. As it explodes the flat row bends into a travelling sine wave of faces that scrolls and undulates, with the lead name settling on the last follower and the count landing on the total. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { FollowerRush } from "@/components/snap-cn/follower-rush"; const Rush = () => ( ); export const RemotionRoot = () => ( ); ``` ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "orientation", type: "\"horizontal\" | \"vertical\"", default: "\"horizontal\"", description: "Layout orientation — horizontal is 1280×720 (16:9), vertical is 720×1280 (9:16) letterboxed", }, { name: "speed", type: "number", default: "1", description: "Animation speed multiplier — minimum 1 so the timeline always reaches the wave", }, ]} /> ## Notes - **Exponential count:** The total holds at 1 through the inline intro, then grows on an eased exponential — a slow crawl to a couple dozen, then a blow-up that lands exactly on `totalFollowers`. - **Flat pile → wave:** Avatars overlap in a centred pile (leftmost on top, a page-coloured ring separating them), then bend into a travelling sine wave that scrolls; a single edge mask fades the wave's ends. - **Offline rendering:** Avatars are real low-id GitHub photos (CORS-safe), each backed by an initial-letter circle, so the composition renders with no network and MP4 export needs no CORS setup. - **Live data ready:** Pass your own crowd as `followers` in the `{ name, avatarId }` shape; the lead callout name cycles through it and freezes on the last one. --- # Scenes URL: https://snapcn.dev/docs/scenes Full scene compositions for launch and demo videos Complete beats of a product video — openers, problem statements, pricing, and outros. Drop them into a `` or `TransitionSeries` and swap the copy for your own. --- # Announce Title URL: https://snapcn.dev/docs/scenes/announce-title A four-shot announcement title — the eyebrow rushes past the camera on a receding type plane, the name assembles on paper, and a macro pan cuts wide as the tagline builds itself last word first *(interactive preview: https://snapcn.dev/docs/scenes/announce-title)* ## Installation ```bash npx shadcn@latest add @snapcn/announce-title ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage Three strings and it is a launch film. Everything else has a measured default. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { AnnounceTitle } from "@/components/snap-cn/announce-title"; const Launch = () => ( ); export const RemotionRoot = () => ( ); ``` ## Four shots, three cuts Nothing here crossfades, and nothing is continuous across a cut. **The rush** (frames 0–17). The eyebrow is laid on a plane raked away from the viewer and thrown past the camera. It starts 53× its resting size with the plane's horizon *inside the frame* — one letter fills the screen — and lands at 4× as the horizon leaves and the plane flattens to face-on. Magnification and rake are both frame-by-frame tables rather than curves, because the move slows to a near-hold around frame 12 and then speeds up again into the cut, which no single easing does. **The field** (18–40). The same word, flat, finishing the move: 1.530× down to rest, its excess shrinking 24% per frame. Then it is torn off to the **left**, displacement tripling every frame, staggered about a fifth of a frame per letter — by frame 38 the first letter has travelled 56px and the last one five. **The paper** (41–79). Each word of the name slides in from the right into a slot that is already laid out. They never push each other around, they all close at the same 0.82 per frame, and the lead word travels 1.63× as far as the rest. Each pops in at partial opacity rather than fading up from nothing. **The macro pan and the close** (80–169). A macro shot of the middle of the tagline pans left — decelerating from 52px/frame to 9 at the hold, then accelerating out to 105 — with the symbol crossing the other way at the same speed. It cuts wide, and the line builds itself **last word first** while a white sweep crosses it left to right and drains the colour out of it. ## Making it yours ### The symbol The mark that leads the closing line is a path in a `0 0 100 100` box — it ships as the snapcn mark, and any other drops straight in: your logo, a bullet, a glyph you traced. ```tsx ``` `symbolColors` are gradient stops spaced evenly from the mark's first tip to the last — two give a plain ramp, one a flat fill, and leaving it out keeps the ten measured stops of the default spark. The customizer's text field takes the same thing comma-separated. `symbolPath=""` removes the mark entirely. ### Colour A title sequence is burned-in film type, not app chrome, so its palette is the design rather than the design system — every ground and ink is a prop: ```tsx ``` ### Length Type sizes are measured to the pixel, and a size fixed to one sentence clips the moment you write a longer one. So each line is drawn once into a hidden probe, its real width read off it, and its size scaled down only if it would overrun its share of the frame — a line that already fits renders at exactly the measured size. A character count cannot do this: the default tagline is one character longer than the one everything was measured against and **14% wider**, because `Ready-made` and `Remotion,` carry wider glyphs than `Perfect` and `massive`. ## Measured Every number below came off rendered frames, not taste. | | measured | | --- | --- | | cuts | frames `18`, `41`, `80`, `110` | | opening magnification | `53×` → `4×`, rake `81°` → `14°` | | eyebrow settle | `1.530×`, decaying `0.7605` per frame | | eyebrow exit | `×3.2` per frame, leftward, `0.215` frames per letter | | title words | `0.82` per frame; lead word travels `1.63×` the others | | macro pan | `52` → `9` → `105` px/frame | | closing slide | `0.792` per frame | | word reveal | one word every `2.833` frames, right to left | | white sweep | `31.7` px/frame, left to right, `0.21 W` soft edge | The settles are all the same curve — a first-order exponential lag, not a cubic-bezier — normalised over a fixed window so it *lands* instead of spending its last five frames moving less than half a pixel. ## Props This scene plays as a rendered mp4 rather than a live ``. Type is under a moving scale for most of the run — a receding 3D plane, a 1.5× settle, then a whole line panned at 14× — and the first two shots are each drawn up to 44 times a frame for the shutter. A browser has an 8ms budget for all of that; a render has none, and the difference is the whole read of the opening. Touch any control and the customizer hands you the live Player back. --- # Product Hero URL: https://snapcn.dev/docs/scenes/hero-launch Cinematic product-launch hero — two cards slide into formation as the headline reveals above *(interactive preview: https://snapcn.dev/docs/scenes/hero-launch)* ## Overview A launch-style hero on one shared timeline: a dark product card fades and scales into the centre, eases left until about half of it remains visible, and a white feature card slides in from the right to meet it. Both cards then drop together as a group while the headline reveals above — fading up from below with a blur that resolves as the cards settle. Every beat overlaps the next, using high-damping springs (no overshoot) for a premium, non-robotic feel. Pass your own `heading` to change the headline. --- # Moodboard Reveal URL: https://snapcn.dev/docs/scenes/moodboard-reveal A kinetic headline, a scattered photo gallery, a camera push from dark to light, landing on a hero image *(interactive preview: https://snapcn.dev/docs/scenes/moodboard-reveal)* ## Installation ```bash npx shadcn@latest add @snapcn/moodboard-reveal ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage A four-beat, camera-drives-through montage: 1. **Headline** — `` · an inline image that swaps · **``** · ``, on a near-black dotted field. 2. **Gallery** — the image cards fly in and scatter into a moodboard. 3. **Push** — the camera dollies through the gallery (with motion blur) while the background lifts from near-black to off-white. 4. **Hero** — it lands on a single image, held to the end, with thin wireframe markers drifting. Pass your own `images` (used for both the intro swap and the gallery) and a `heroImage`. Everything is frame-driven, so scrubbing and rendering are deterministic. ```tsx // src/Root.tsx import { Composition, staticFile } from "remotion"; import { MoodboardReveal } from "@/components/snap-cn/moodboard-reveal"; const Reel = () => ( ); export const RemotionRoot = () => ( ); ``` Images take a root-relative `/…` path (served by Next in the Player, rewritten through `staticFile()` in a render) or an `http(s)`/data URL. Eight portrait images fill the gallery cleanly; fewer are tiled across the slots. This is a slow camera scale over the whole gallery, which a live `` mispaces on a high-refresh display — so the docs preview plays the **rendered mp4**. Re-render after any change: `pnpm run render:previews --only moodboard-reveal`. ## Props ", description: "Design-system token overrides. Anything you leave out falls back to the shadcn defaults", }, { name: "mode", type: "\"light\" | \"dark\"", description: "Which end of the design system to resolve tokens against", }, ]} /> --- # Orbit Gallery URL: https://snapcn.dev/docs/scenes/orbit-gallery An image-universe hero — a stream of photos flows along an Archimedean spiral vortex from the frame edges into the center, evenly arc-spaced, shrinking and rotating along the coil, with the title held in the clear middle *(interactive preview: https://snapcn.dev/docs/scenes/orbit-gallery)* The editorial "universe" opener: a continuous stream of photos flows along an **Archimedean spiral** — a vortex seen from the top. Cards enter big at the frame edges (the outer coils overflow and clip), then wind inward, shrinking and rotating to follow the spiral's tangent until they vanish at the center. Because the spiral's radius falls off linearly, every coil is separated by the **same gap** — that even "circle to circle" spacing — and cards are placed by **arc length**, so they stay evenly spread instead of bunching near the middle. The center stays clear for the title. ## Installation ```bash npx shadcn@latest add @snapcn/orbit-gallery ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage ```tsx // src/Root.tsx import { Composition } from "remotion"; import { OrbitGallery } from "@/components/snap-cn/orbit-gallery"; const Opener = () => ( ); export const RemotionRoot = () => ( ); ``` Works in any aspect ratio — the spiral radius derives from `min(width, height)`, so the vortex scales cleanly across portrait, square and landscape. ## How the vortex is built `archimedeanPoint(n, R, turns)` walks the spiral from the outer edge (`n = 0`, radius `R`) to the center (`n = 1`, radius 0) — a linear radius, so the coils are evenly spaced. `buildArcTable` / `arcToN` reparameterize the path by arc length so cards sit at even visual intervals, and `sizeScale` shrinks each card toward the middle. The stream then advances by whole card-steps per loop, so the inward flow is seamless. Cards are sorted by depth (center on top) and rotated to the spiral's tangent. ## Props - `images` — artwork URLs, cycled along the stream. A URL that fails to load falls back to a gradient tile (playback never breaks). Pass `[]` to use the built-in editorial gradient tiles (no network requests). - `turns` — coils from edge to center (default 3.5). - `spacing` — arc gap between cards; smaller packs them tighter (default 5). - `spread` — radius scale; larger pushes the outer coils further off-frame while keeping the coil gaps even (default 6). - `imageSize` — card long-edge in px on the reference 600px canvas, scaled to your frame (default 200); `sizeAttenuation` controls the center shrink. - `cornerRadius` — 0–20 rounding (default 5; `0` for perfectly square cards). - `orbitSeconds` — seconds for one full inward turnover (default 40). - `title` / `subtitle` / `buttonLabel` — the centered lockup; empty `title` hides it for a pure vortex. - `footerLeft` / `footerCenter` / `footerRight` — tiny bottom captions. Pairs well with a title opener as the establishing beat. --- # Status Cycle URL: https://snapcn.dev/docs/scenes/status-cycle A status pill whose label rolls behind a hard clip while its width springs past the target and back, then the field crossfades to a column of chips stepping up from below *(interactive preview: https://snapcn.dev/docs/scenes/status-cycle)* ## Installation ```bash npx shadcn@latest add @snapcn/status-cycle ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage A prefix, a list of statuses and a list of chips. Everything else has a measured default. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { StatusCycle } from "@/components/snap-cn/status-cycle"; const Cycle = () => ( ); export const RemotionRoot = () => ( ); ``` `durationInFrames` has to cover the whole arc: the intro, one `statusHold` per status, the crossfade, and one `chipStagger` per chip. The default 150 fits the default content with a hold at the end; add roughly 18 frames per extra status and 8 per extra chip. ## How the swap works Three things happen on the same frame, and keeping them on the same frame is most of the effect. **The label rolls.** The outgoing label translates up and out through the pill's top edge while the incoming one arrives from below, both hard-clipped by the pill's rounded box. There is no crossfade — the label's ink never dims — and the travel is one pill height. **The pill's width springs past its target.** Not a spring, though: the curve is `cubic-bezier(0.65, 0.2, 0.3, 1.5)`, and the `y2 > 1` control value *is* the overshoot. A real spring cannot make this shape. One frame into an eleven-frame rise a spring released from rest is already 4.1% of the way there; the reference is at 0.65%, six times flatter, and no damping/stiffness pair fixes that without losing the early peak. **The line re-centres.** The lockup is centred as a whole, so a widening pill pushes the prefix left and a narrowing one lets it back. The prefix does not re-animate; it only moves. The label is laid out at its full final width and centred, and the widening pill reveals it from the middle outward — so during a morph you see the middle of the new label with both ends clipped. That is why the width is its own animated quantity and is never derived from a padded text box. ## Content of any length Every width in the scene is measured at runtime behind `delayRender()` — the prefix's glyph advances, each status label, each chip — so a longer status or a longer chip simply makes a wider box. Nothing is tuned to one sentence. Every other proportion is a fraction of the composition rather than a pixel, so the scene holds its shape at 720p, 1080p or a vertical crop. ## Colour Leave the colour props off and the scene paints from the design system: `primary` for the first field, `background` for the second, `foreground` for the pill and the ink, and a pale tint of the field for the pill's label. Pass any of them to override. ```tsx ``` ## Overriding the motion Every curve and duration is a measured default on the exported `STATUS_CYCLE_MOTION`, and `motion` is merged over it — so you override one without restating the rest. ```tsx import { Easing } from "remotion"; ``` ## Props ", default: "STATUS_CYCLE_MOTION", description: "Curve and timing overrides: widthEase, widthSeconds, rollEase, rollSeconds, rollTravel, fadeSeconds, exitSeconds, scrollTau, letterSpring, letterStagger. Every default is fitted from frame data, so overriding one is a departure rather than a tweak", }, { name: "fontSize", type: "number", default: "8.7% of the composition height", description: "Act-1 type size in px", }, { name: "chipFontSize", type: "number", default: "22.2% of the composition height", description: "Act-2 type size in px", }, { name: "fontFamily", type: "string", default: "Inter", description: "Loaded through @remotion/google-fonts, so the Player and the render agree", }, { name: "introFrames", type: "number", default: "24", description: "Frames the per-letter intro cascade gets before the first status swap", }, { name: "statusHold", type: "number", default: "18", description: "Frames from one status swap to the next. Shorter than the 18-frame width morph on purpose — the next swap starts while the last is still settling", }, { name: "chipStagger", type: "number", default: "8", description: "Frames from one chip's arrival to the next", }, { name: "startAt", type: "number", default: "0", description: "Frame the scene starts on", }, { name: "speed", type: "number", default: "1", description: "Playback rate. Below 1 the last chip never arrives", }, { name: "theme", type: "Partial", description: "Design-system token overrides", }, { name: "mode", type: '"light" | "dark"', description: "Which side of the token set to resolve", }, ]} /> --- # Showcase URL: https://snapcn.dev/docs/showcase Videos built with snapcn, submitted by the community. Videos other people built with snapcn, at https://snapcn.dev/docs/showcase. Sign in to submit one — either a link to where you posted it, or an export straight from the video editor, which is then hosted here. Submissions are reviewed before they appear. --- # Video Editor URL: https://snapcn.dev/docs/video-editor Compose a video from snapcn components — add clips, edit text and images, and export an MP4. A browser video editor at https://snapcn.dev/docs/video-editor. No install, no Remotion project needed — it is the fastest way to see what the components do. - Add clips from the registry to a timeline, edit their text, images and colours, reorder them and set each one's length. - Add a soundtrack, and trim where it starts. - Export an MP4, rendered server-side with Remotion: 1280×720 at 30fps, up to 60 clips and 180 seconds in total. - Exports carry a small snapcn mark unless you are signed in and turn it off. A render you run yourself, from installed components, is never marked. - Signed in, every timeline is saved as a project you can reopen. --- # Changelog URL: https://snapcn.dev/docs/changelog Every component in snapcn, by the day it landed. Every component, by the day it landed. Newest first. Also available as a feed: https://snapcn.dev/docs/changelog/rss.xml - **2026-08-25** — Type Morph - **2026-08-23** — Block Wordmark - **2026-08-20** — Announce Title, Status Cycle - **2026-07-28** — Answer Stream - **2026-07-26** — Prompt Zoom - **2026-07-24** — Text Reveal, Text Swell, Text Highlight, Text Swap, Text Build, Word Flip, Word Captions, Karaoke Captions, Logo Assemble, Logo Flicker, Phone Frame, Laptop Frame, Terminal Simulator, Follower Rush, Product Hero, Orbit Gallery, Moodboard Reveal, Search Typing --- # Roadmap URL: https://snapcn.dev/docs/roadmap What snapcn is, what is being built next, and what is only an idea so far. What is built, what is being built, and what is only an idea. Dates appear only on work already in progress; "exploring" may never ship. ## Shipped - **The component registry**: 24 components — text animators, captions, logo stings, device frames, social proof and full scenes — installable with one shadcn command. Listed in the shadcn registry directory, so the CLI resolves @snapcn with nothing to configure. https://snapcn.dev/docs/components - **snap-cn-ui primitives**: Timeline-driven versions of the shadcn atoms — input, caret, and the theme and colour core the scenes paint from — so a component drops in next to your own Input and Button and belongs there. https://snapcn.dev/docs/getting-started/installation - **The agent skill**: A reference an AI coding agent can read: every component, its props, and the archetypes they compose into. Point Claude at it and ask for a product demo. https://snapcn.dev/docs/getting-started/agent-skill ## Building - **A new clip every day**: The registry is the product and it is not finished. One new clip, published every day — the gap between what a demo video needs and what is in here closes by cadence rather than in batches. The changelog is the receipt. https://snapcn.dev/docs/changelog - **Templates** (due 2026-09-02): Whole videos rather than single scenes — a launch film, a feature walkthrough, a changelog clip — composed from the registry and ready to render once you drop your own copy in. https://snapcn.dev/docs/templates - **Video editor** (due 2026-08-26): Compose a video from snapcn components in the browser — add clips, edit the text and images, export an MP4. The timeline and the server-side render path already exist; what is left is making an export something you would actually ship. https://snapcn.dev/docs/video-editor ## Exploring - **Marketplace**: A place for components that are not ours — other people's scenes, installed the same way. Only worth building once there are enough authors to fill it. https://snapcn.dev/docs/marketplace --- # Documentation URL: https://snapcn.dev/docs snapcn is a shadcn registry of Remotion components — install an animation into your own project and own the file. You have shipped the product and now you need the video. snapcn is a [shadcn](https://ui.shadcn.com) registry of Remotion components — titles, captions, device frames, logo stings, whole scenes — that you install into your own project with the CLI you already use. The component lands in your repo as a file you own, written against the plain Remotion API, and you edit it like any other component you wrote yourself. ## How it works 1. **Have a Remotion project.** snapcn does not bootstrap one — `npx create-video@latest` does. See [Installation](/docs/getting-started/installation). 2. **Add a component.** `npx shadcn@latest add @snapcn/text-reveal` copies the source into `components/snap-cn/`, with anything it depends on. No dependency on snapcn is added to your `package.json`. 3. **Put it in a composition.** Pass the words you are animating; every other prop has a working default, so it renders the moment it is mounted. Change the copy, the colours, the timing — it is your file now. 4. **Render.** `npx remotion render`. The same component that played in the browser preview is the one that goes into the mp4. ## What is in here [Components](/docs/components) is the gallery: every component in snapcn in one filterable grid, each card playing its own scene. The category pages below are the same components grouped by the job they do, with the installation and usage notes for that group. Every component is MIT licensed, and the code is [on GitHub](https://github.com/snapcndev/snapcn). --- # AI Chat Input URL: https://snapcn.dev/docs/ai-input Prompt bars, search fields and chat composers that type themselves — the shot where a product gets asked a question. Reach for this category when the input *is* the shot: the search bar at the top of a landing page, the prompt box in an AI product, the composer someone is about to hit enter on. These components animate the query being written — and the camera that leans in to watch it happen. The hard part is not the typing, it is the type. A field that resizes as it fills, or a line of text that re-kerns as it grows, or a scale that drags the baseline across the pixel grid, is what makes a demo look cheap. These components lay the sentence out once and reveal it by clipping, and they pivot every scale on the baseline — so the letters land, and nothing else moves. --- # Answer Stream URL: https://snapcn.dev/docs/ai-input/answer-stream The beat after send — a macro shot on the button that cuts hard to the answer building itself, while the camera pulls back to keep up with it *(interactive preview: https://snapcn.dev/docs/ai-input/answer-stream)* ## Installation ```bash npx shadcn@latest add @snapcn/answer-stream ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage It opens where [Prompt Zoom](/docs/ai-input/prompt-zoom) leaves you — close on the composer — and carries through the send, the cut, and the answer. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { AnswerStream } from "@/components/snap-cn/answer-stream"; const ReplyScene = () => ( ); export const RemotionRoot = () => ( ); ``` ## Scene to scene, the frame cuts Read frame by frame, the reference has exactly one grammar for changing shot, and it is a **hard cut** — one frame wide, no blur, no ramp. What makes the cuts invisible is not a transition. It is what happens either side of them. **Every cut lands on motion.** The camera is eight frames into an accelerating push toward the send button when the cut fires. It is moving fastest at the moment it is replaced. **Every cut is followed by a glide, not a stop.** The answer page arrives 27px high and settles down over 15 frames on an ease-out. Cut to a static frame and the edit becomes the loudest thing in the shot. The only *moves* are inside a shot, and there are two kinds: a slow drift that never quite stops, and one fast blurred push onto whatever is about to happen. ## The pull-back is fitted to the content As the answer grows — paragraph, then headline, then four cards, then the composer sliding up — the camera pulls back and rises to keep the block in frame. Tracking the column's left and right edges across the move recovers a scale of **1.364× → 1.0** about a fixed point at **(0.5, −0.548)** of the frame: dead centre horizontally, and 273px *above the top edge*. Both edges independently agree on that point to within 1px, which is what makes it a measurement rather than a guess. The focal point being above the frame is the whole reason the page appears to **rise** as it shrinks, instead of collapsing toward its own middle. Peak velocity sits at 48–52% of the move, which is a cubic in-out and rules out a spring — a spring peaks at a third. Measured back off the rendered frames against the reference's, the two curves start together, end together and part by at most 0.048 of scale near the midpoint. `pullbackFrom` is expressed against where the shot **settles**, not where the pull-back bottoms out. It overshoots its mark by 2.8% and creeps back onto it, and normalising against the bottom of that move instead puts every constant 2.8% out. The move carries **motion blur** derived from the camera's own speed — a frame that carries a typical element `d` px smears it by `d/4` — rather than dialled in by eye. ## Words arrive hot Containers land empty and fill: the question pill and each of the four cards appear as a bare surface first, and their text streams in after. Each word lands at `accentColor` and cools to `foreground` on a fixed clock, so the lit band at the head of the stream is `wordsPerSecond × coolSeconds` words wide — six, on the reference — no matter how fast you run it. Words that have not landed are hidden, not absent. Removing them lets the line reflow underneath and every word already on screen shifts as the next one arrives; on the reference they do not move at all. ## Props | prop | default | | | --- | --- | --- | | `question` | — | fills the pill, and stays in the composer | | `answer` | — | `\n` is a hard break; everything else wraps | | `headline` | — | lands under the reply, introduces the cards | | `cards` | four | `{ title, body, icon? }` — `icon` is an SVG path on a 24×24 box | | `commitAt` | `1.0` | when the push into the button starts accelerating | | `cutAt` | `1.284` | when the shot cuts. One frame, never a ramp | | `pullbackAt` | `1.933` | when the camera starts keeping up | | `pullbackDuration` | `1.1` | | | `pullbackFrom` | `1.364` | relative to where the shot settles | | `pullbackUndershoot` | `0.028` | how far past its mark it goes first | | `focusX` / `focusY` | `0.5` / `-0.548` | above the frame, on purpose | | `macroZoom` | `2.36` | the opening shot on the send button | | `wordsPerSecond` | `25` | | | `coolSeconds` | `0.23` | how long a word stays lit | | `blur` | `3` | ceiling on the camera's motion blur | Plus `accentColor`, `theme`, `mode` and `speed`, like every other component. ## Notes The preview above plays a **rendered mp4**, not a live ``. The pull-back re-shapes every line of type on the page at a new size for 33 consecutive frames; a frame that misses its budget in the browser is shown for the wrong length of time, and the eye reads that as the answer sticking as it arrives. The reference has no button press — its cursor is already down when the shot opens, and the accelerating push does all the work. Synthesised, the cut needs a visible cause, so there is a three-frame dip on `commitAt`. The colours and the copy are **not** measured. The reference is a specific product with its own coral, its own four icon tints and its own marketing lines; none of that belongs in a component that lands next to somebody else's `Input`. The paint is the shadcn token set, the composer takes its surface from the `Input` primitive's own style context, and every string is a prop. --- # Prompt Zoom URL: https://snapcn.dev/docs/ai-input/prompt-zoom A prompt field that types a sentence into itself and, partway through, cuts — hard, in a single frame — into the caret and keeps going *(interactive preview: https://snapcn.dev/docs/ai-input/prompt-zoom)* ## Installation ```bash npx shadcn@latest add @snapcn/prompt-zoom ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage The prompt starts typing in the wide shot. Partway through the sentence the view **cuts** to the caret — and the typing does not pause for it, it carries straight on at the new size. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { PromptZoom } from "@/components/snap-cn/prompt-zoom"; const AskScene = () => ( ); export const RemotionRoot = () => ( ); ``` ## The cut is the point It is a **cut, not a zoom**. On the reference the wide view is whole on one frame and fully pushed in on the very next — 803ms to 824ms, with nothing in between. Interpolating it, even across two or three frames, turns a cut into a move and loses the snap entirely. `isZoomed` is deliberately a boolean, and there is a test asserting no frame is ever partway. It fires **mid-sentence**, and that is what keeps it from reading as a scene change: the frame either side of the cut shows the same half-typed prompt, so there is one continuous action running through it and the cut simply gets you closer to it. Where it lands matters as much as how fast. The push is **2.547×**, anchored on a point **10% across and 41% down the input field** — the text insertion point. The shot doesn't just get bigger, it arrives on the thing that is about to happen. That focal point was fitted to the field's top and left edges, then checked against geometry it had not seen: it predicts the `+` glyph's centre to within **1.2px** and the field's bottom edge to **0.0px**. | | measured | | --- | --- | | typing starts | `0.35s` | | cut at | `1.0s` — mid-sentence | | zoom | `2.547×` | | focal point | `(0.270, 0.516)` of the frame | | typing | `18` chars/sec | Every one of those is a prop. ## Colour and wording The proportions and the timing are measured. The **colours and the labels are not** — the reference is a specific product, and its mark, model names and suggestion labels are its brand, not something to ship to strangers. The paint is the shadcn token set, the field takes its surface from the `Input` primitive's own style context so the two cannot drift, and `greeting`, `placeholder`, `model` and `effort` are all props. Suggestion pills are off by default — pass `chips={["Write", "Learn"]}` to show a static row under the field. `accentColor` paints the starburst and defaults to `theme.primary`. The reference clip is 1.078s and stops **mid-word** — it is still typing "Get…" when it ends. The default duration here is the choreography's own end plus a hold, not the recording's length. --- # Search Typing URL: https://snapcn.dev/docs/ai-input/search-typing A search field wider than the shot — two thirds in frame, then it comes forward, types across its left half, pages to its right half, and goes back *(interactive preview: https://snapcn.dev/docs/ai-input/search-typing)* ## Installation ```bash npx shadcn@latest add @snapcn/search-typing ``` ## Component code The exact file `shadcn add` copies into your project. ## Usage Two thirds of the field are in frame to begin with, parked back. On the first keystroke it comes forward, and now only its **left half** fits. The sentence types across that half; when the caret runs out of frame the field slides, and its **right half** comes in. The sentence finishes there — and then the field goes back **further than it started**, far enough that the **whole** of it lands in frame, centred. That is the only moment in the clip you see the complete field and the finished sentence at once, so it is the shot the whole thing is built toward. Everything is driven by the current frame, so scrubbing and rendering are deterministic — including the keystroke jitter, which is seeded, not sampled from a clock. ```tsx // src/Root.tsx import { Composition } from "remotion"; import { SearchTyping } from "@/components/snap-cn/search-typing"; const AskScene = () => ( ); export const RemotionRoot = () => ( ); ``` The component ships with a transparent background — place it over any backdrop, or over one of the [Backgrounds](/docs/backgrounds). ### The field is wider than the frame That is the whole idea. `fieldHeight` sets how tall the field reads on screen (0.27 of the frame, so it comes out long and slim), and `frontVisible` sets how much of it is in frame once it has come forward. The field is then **padded out to whatever length that framing needs**. That padding matters, and it is not a fudge. A field only as long as its sentence has a *fixed* aspect ratio — so demanding that half of it fill the frame would force its height, and it would come out as a chunky slab. Separating the two knobs is what lets it be both half-in-frame and slim. A real search field is longer than what you type into it anyway. `dolly` is the ratio between the two depths — **1.25**, which is what the reference measures. `frontVisible × dolly` is how much is in frame at rest: 0.56 × 1.25 ≈ **two thirds**. The **ending depth is not a third knob**: it falls out of the geometry. The field retreats until it is exactly as wide as the page, so it fits — and because `edgeInset` leaves an equal margin either side, anchoring the retreat on the field's right cap lands it centred with no special case. The push and the retreat are therefore two separate moves to two separate depths (`cameraPush`, `cameraRetreat`), not one move played backwards. Folding them together is what kept the field from ever pulling back far enough to be seen whole. The page across is not on a timer — it fires on the **character whose caret is about to run out of frame**, which falls near the middle of the sentence. It fires a little early on purpose: the pan eases in, so for its first frames the field has barely moved while the typing carries the caret on rightwards. Fire it *at* the edge and the caret spills over before the field catches up. Short sentences that fit in the first half never pan at all. ### Proportions Every measurement is a ratio of the field's height, so the whole thing scales as one piece. They are not invented: they come off the reference frames. | | ratio of field height | | --- | --- | | corner radius | **0.276** — not a pill; a pill would be 0.500 | | font size | 0.379 (cap height 0.265) | | left cap → first glyph | 1.024 | | left cap → icon | 0.357; icon 0.378 | | caret | 0.024 wide × 0.434 tall | The face is **Outfit Light (300)**, loaded through `@remotion/google-fonts` so the Player, the rendered mp4 and your own project all get the same one. It was chosen by measurement, not by eye: the reference's stem-to-cap ratio is 0.121, and Outfit 300 is the geometric face that lands on it. ### The surface is the design system's, not the reference's The proportions above are measured. The **colours are not** — they were the reference's brand. The field paints itself from `inputStyleContext()`, the very same style context the shadcn `` uses, so it is a card surface on a hairline border and it cannot drift away from the input sitting next to it in your app. That matters most for the shadow. The reference's field carries a heavy drop shadow and a grey-crown gradient. On its dark violet backdrop those read as depth and as glass; on a light page they read as a **grey smear** and an **inner shadow**. So the default carries **no shadow at all** — a flat `#FFFFFF` card and a `#E4E7EC` hairline border, which is how shadcn defines a control. `surface="glass"` keeps the measured reference field; use it only over something dark. ### Why the type doesn't judder as it scales A browser gives glyph origins **no vertical sub-pixel precision at all** — it rounds them to a whole device pixel. So a scale that *moves the baseline* makes the type climb the pixel grid in whole-pixel jumps: sit still, jump, sit still. So the scale **pivots on the baseline**, which is measured (a zero-sized inline-block sitting on it), never guessed from a line-height. On the rendered frames the baseline holds to a **single pixel row** across the full 1.25× sweep. Separately, hinting bends each glyph so its stems land on whole pixels, and under a sliding scale they re-snap every frame — the letterforms boil. `text-rendering: geometricPrecision` turns that off. `will-change: transform` is gated on `getRemotionEnvironment().isRendering`: right for the Player (one long-lived tab, a hard frame budget), wrong for a render (parallel tabs each inherit a stale raster). ### Why the text doesn't shift as it types The obvious way to type a string is `text.slice(0, n)`. That re-shapes the run every keystroke, and a run that re-shapes can re-kern — settled glyphs twitch as new ones land. Instead the sentence is laid out **once, in full**, and revealed by clipping to a measured character boundary. A run shaped once cannot move a glyph. Ligatures are off, so the clip can never bisect an "fi". The measurement runs once, behind `delayRender()`, after `document.fonts.ready` — and with the camera transform switched off, because `getBoundingClientRect()` reports device px and would otherwise hand back every advance multiplied by the current scale. ## Props ", description: "Design-system token overrides — the same shape every snap-cn-ui component takes", }, { name: "mode", type: '"light" | "dark"', default: '"light"', description: "Which default token set to resolve against", }, { name: "icon", type: '"search" | "sparkle" | "none"', default: '"search"', description: "Glyph in the left of the field", }, { name: "fontFamily", type: "string", description: "Overrides Outfit", }, { name: "fontWeight", type: "number", default: "300", description: "CSS font-weight. The reference measures as a Light", }, { name: "seed", type: "string", default: '"search-typing"', description: "Seeds humanize. Same seed, same performance, forever", }, { name: "speed", type: "number", default: "1", description: "Global playback multiplier applied to the whole sequence", }, { name: "className", type: "string", description: "Optional className passed to the field", }, ]} />