snapcn
28 components
DocsComponentsTemplatesVideo EditorNewShowcaseMarketplaceRoadmapChangelog

Getting started

IntroductionInstallationAgent skill
GitHub

Star snapcn

Free, open-source components.

Production-ready Remotion animations, transitions and backgrounds — you own the code.

Star on GitHubXFollow

MIT licensed · own your code.

© 2026 snapcn

Loading
snapcn
28 components
DocsComponentsTemplatesVideo EditorNewShowcaseMarketplaceRoadmapChangelog

Getting started

IntroductionInstallationAgent skill
GitHub

Star snapcn

Free, open-source components.

Production-ready Remotion animations, transitions and backgrounds — you own the code.

Star on GitHubXFollow

MIT licensed · own your code.

© 2026 snapcn

DocsComponentsTemplatesVideo EditorNewShowcaseMarketplaceRoadmapChangelog
Getting startedIntroductionInstallationAgent skill

Text Rewrite

A line writes itself, is selected, then has its opening swept away by a clip that cuts letters in half — and a new ending written on while it slides to its new centre

Installation

pnpm
$ pnpm dlx shadcn@latest add @snapcn/text-rewrite

Component code

The exact file shadcn add copies into your project.

Usage

// src/Root.tsx
import { Composition } from "remotion";
import { TextRewrite } from "@/components/snap-cn/text-rewrite";

const Claim = () => (
  <TextRewrite headline="Or just add snapcn" keep={1} append="registry" />
);

export const RemotionRoot = () => (
  <Composition
    id="TextRewrite"
    component={Claim}
    durationInFrames={102}
    fps={30}
    width={1280}
    height={720}
  />
);

The whole gesture is three props. keep counts words off the end — everything before them is what gets swept away:

<TextRewrite
  headline="Ship it by hand or ship it with snapcn"
  keep={1}
  append="in an afternoon"
  accentColor="#7c3aed"
/>

The erase is a clip, not a deletion

This is the measurement everything else follows from. As the opening of the line is taken away, letters are cut in half by a vertical edge — "Or" becomes ")r" becomes "r" — and the words that survive never move a pixel. A word-by-word delete would have re-laid the line out on every step, and across 183 frames the recording plainly does not.

So there is no deletion here at all. There is a window with overflow: hidden whose left edge sweeps right over text that is standing still.

The rewrite is that same edge, run the other way

The window's right edge then sweeps right, revealing the new ending a fraction of a letter at a time, while the line slides to where its new content belongs.

Those are two independent measurements — the clip edge, and the line's centre — and they land on one shared progress to within 0.001 of the travel. Fitted separately:

fittedrms
erase (the clip edge, 29 frames)cubic-bezier(0.419, −0.050, 0.184, 1.044)0.0086
rewrite (the line's centre, 31 frames)cubic-bezier(0.427, 0.021, 0.193, 0.999)0.0059

Two different signals, two different halves of the gesture, the same curve. It is cubic-bezier(0.42, 0, 0.19, 1), and it is an ease-in-out — which is the whole character of the move. The edge creeps for four frames, crosses the sentence in twelve, and settles for twelve more. An ease-out would have read as a deletion. This reads as something being read, and then taken away.

Every curve here was fitted on the recording's timestamps, not its frame numbers. It is a 60fps capture with eight dropped frames, so a frame index is not a clock — fitting on frame index makes the erase 0.467s when it is really 0.517s, and everything downstream lands early.

The mark rides the edge

The trailing mark sits 0.4em clear of the last visible letter, not of the end of the line — through the typing, through the erase, and through the rewrite, where the last visible letter is often only half a letter. Measured, it is a constant 54px from the window's right edge on every frame of the gesture.

That is what keeps the whole thing reading as one object rather than as a line with a decoration next to it. The default is the snapcn mark; markPath takes any path drawn in a 0 0 100 100 box, and markSize={0} leaves the line bare.

The line opens, it does not reflow

When a word lands, both ends of the line move outward by the same amount over the same five frames — +18, +8, +4, +2, +1 px on the right and the mirror of that on the left — while the centre never leaves 404 of 810. The word itself is drawn at full width from its first frame and fades: on the reference it lands whole and pale, overlapping the mark, and the mark slides clear over the next four frames.

So the line's width is tweened around a fixed centre. Nothing is clipped while the line is being written, and nothing jumps.

The size change is a cut

188px of ink on one frame and 453px on the next — 2.41×, with no intermediate frame anywhere in the recording. zoomDuration={0} is that, and it is the default here because it is what the reference does.

Give it a duration and it becomes a smooth push, and the pivot moves with it: a cut has no in-between frame to judder on, so it pivots on the line's optical centre (which is what the reference does — its small line and its full-size line share a centre at y 224 of 458, not a baseline). A ramp does have in-between frames, and there the pivot has to be the measured baseline, because the rasteriser gives glyph origins no vertical sub-pixel precision and any pivot that moves the baseline makes the type climb the grid in whole-pixel jumps.

<TextRewrite zoomDuration={0.2} />  // a push instead of a cut, pivot follows

The selection, and its sheen

The drag is 28% across by 17ms, 54% by 33ms, 71% by 50ms — an easeOut of degree 3.5 over 200ms, and it starts on the same frame the line comes forward. That was measured without touching the gradient at all: take nineteen fixed columns across the line and find the frame the warmth arrives in each. A quint, the obvious guess, is far too fast off the mark and misses the first two points by twenty points of travel.

The fill is a gradient, and it is measurably the same one on every frame it appears: read column by column across eight frames spanning three box widths and two phases — 402px selected, 272px after the rewrite, 193px mid-erase — and it lands in the same place every time. Deepest at 0.36, palest at 0.575, a 3.2× swing. It is painted on the box rather than on the page, which is why it appears to travel as the box is dragged and then eaten.

The deselect is nine frames and linear to within 6% of the travel. Nothing about it eases; the selection is simply dropped.

measured
mark appears0.117s
word stagger189ms
line opens in85ms
forward move1.033s, 2.41×, one frame
drag1.033s, 200ms, easeOut³·⁵
erase1.45s, 517ms, ease-in-out
rewrite2.0s, 533ms, the same ease
deselect2.85s, 150ms, linear
selection box0.954em tall, 0.083em below the baseline
sheendeepest 0.36, palest 0.575, 3.2×

Every one of those is a prop.

Colour and wording

The proportions and the timing are measured. The colours and the wording are not — the reference is a specific product with a specific terracotta and a specific claim, and neither belongs in a component that ships to strangers. The paint is the shadcn token set, the accent and the sheen both come from theme.primary unless you say otherwise, the mark is ours, and the line is a prop.

Props

PropTypeDefaultDescription
headline
string"Or just add snapcn"The line that writes itself. Split on whitespace; each word lands on its own beat and opens the line around a fixed centre
keep
number1How many words off the end survive the erase. Everything before them is swept away from the left
append
string"registry"What gets written onto the surviving words. An empty string leaves them alone and the gesture ends at the erase
markAt
number0.117Seconds at which the mark appears. It leads the first word by a tenth of a second on the reference
wordAt
number0.217Seconds at which the first word lands
wordStagger
number0.189Seconds between words. Also how long a word holds the accent, which is what keeps exactly one word coloured
wordDuration
number0.085How long a word takes to fade up and open the line, on an easeOutCubic
coolDelay
number0.189How long a word holds the accent before it starts cooling
coolDuration
number0.189How long the accent takes to become the foreground
cutAt
number1.033Seconds at which the line comes forward
zoomDuration
number0How long the forward move takes. 0 is the reference's single-frame cut, and pivots on the line's optical centre; any duration makes it a push and moves the pivot to the measured baseline
zoom
number2.41How far forward it goes. 188px of ink became 453px
dragAt
number1.033Seconds at which the selection starts being dragged across — the same frame the line comes forward
dragDuration
number0.2How long the drag takes. The last word also cools over half of it, since it has no next word to hand the accent to
eraseAt
number1.45Seconds at which the clip starts eating the line from the left
eraseDuration
number0.517How long the erase takes
rewriteAt
number2Seconds at which append starts being written on
rewriteDuration
number0.533How long the rewrite takes. The slide and the reveal share it
deselectAt
number2.85Seconds at which the selection is dropped
deselectDuration
number0.15How long the deselect takes
fontSize
number20Type size before the forward move, in stage px. The stage is 810×458 and is scaled to fill the composition
fontWeight
number500Weight of the line. Inter ships 400/500/600 here
centerY
number0.4902Where the line's optical centre sits down the frame, as a fraction
tracking
number-0.012Letter-spacing in em. Every position is measured off the rendered line, so the geometry follows this on its own
wordGap
number-0.05Word-spacing in em, measured the same way
selectionOpacity
number0.21Base strength of the selection. Every sheen stop is a multiple of this
selectionSheen
readonly (readonly [number, number])[]the measured curveThe gradient, as [position, multiple of selectionOpacity] stops. Two stops of [0,1] and [1,1] give a flat fill
selectionAngle
number90Which way the sheen runs, in deg. 90 is the reference's — straight across
selectionPad
number0.11How far the selection reaches past the type on an unclipped end, in em. A clipped end has nothing to pad — a clip is a cut through a glyph
markPath
stringMARK_PATHThe trailing mark, as a path in a 0 0 100 100 box. The snapcn mark by default, and exported so you can spread it
markColor
stringaccentColorIts colour, if it should differ from the accent
markSize
number1.15The mark's box, in em — not its ink, which fills about 70% of it. 0 leaves the line bare
glow
booleantrueThe soft wash behind the line. Static, as the reference's is
glowColor
stringaccentColorColour of the wash, if it should differ from the accent
glowOpacity
number0.075Strength of the wash. On the reference its peak is sixteen luminance levels off the page — a tint, not a shape
accentColor
stringtheme.primaryThe colour a word arrives in, and the colour the sheen is painted from
theme
Partial<SnapCnTheme>—Token overrides — the page colour and the line's settled ink come from here
mode
"light" | "dark""light"Which token set to resolve against
speed
number1Multiplies the frame clock. Every beat above scales with it

The recording is 3.2s and holds the rewritten line for its last fifth of a second. 102 frames is that plus a beat, so the deselect lands and the new claim sits long enough to read before the composition ends.