Next.js — Introducing Next.js
Install this video
Adds the full composition, its remocn dependencies, and the prompt to your Remotion project via the shadcn registry.
$ pnpm dlx shadcn@latest add kapishdima/remocn-demo/introducing-nextjsRender it locally
Renders the MP4 on your machine with the Remotion CLI.
$ pnpm dlx remotion render introducing-nextjs out/introducing-nextjs.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95 --gl=angleThe prompt
Reconstructed draftA reconstruction of the prompt this video was generated from.
The code
The exact source the AI wrote — the same files the install command puts in your project.
import React from "react";
import {
AbsoluteFill,
Easing,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import {
TransitionSeries,
linearTiming,
type TransitionPresentation,
type TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { ShaderGrainGradient } from "@/components/remocn/shader-grain-gradient";
import { SoftBlurIn } from "@/components/remocn/soft-blur-in";
import { Typewriter } from "@/components/remocn/typewriter";
import { pushThrough } from "@/components/remocn/push-through";
import { whipPan } from "@/components/remocn/whip-pan";
import { focusPull } from "@/components/remocn/focus-pull";
import { grainDissolve } from "@/components/remocn/grain-dissolve";
import {
BG,
BRIGHT,
HAIRLINE,
INK,
MONO,
MONO_FAMILY,
MUTED,
SANS,
SANS_FAMILY,
clampOpts,
} from "./theme";
import { GridArc, GridLine } from "./decorations";
import { LogotypeDraw } from "./logotype";
import { OptimizationsIllo, RscIllo, StreamingIllo } from "./features";
import { PoweredByScene } from "./powered-by";
const easeOut = Easing.out(Easing.cubic);
const easeSoft = Easing.bezier(0.2, 0.8, 0.2, 1);
// ---------------------------------------------------------------------------
// Scene timings (frames @ 30fps). The flight: the eclipse triangle hooks,
// the camera dives INTO it and falls through the grain-ring tunnel (the
// introducing-videorc dive, in grayscale) onto the logotype writing itself;
// the nextjs.org hero rebuilds around its own construction lines; the
// receipts glide past; three feature illustrations play as stations; the
// Powered By circuit lights up; the numbers land; one grain cover marks the
// install; the blueprint lockup closes the loop. One grayscale grain
// gradient carries the whole video.
// ---------------------------------------------------------------------------
const S_HOOK = 66; // the opening line, slammed in and held before the eclipse
const S_ECLIPSE = 68; // camera dives into the triangle; the logo-approach takes over
// Kept as S_ECLIPSE = DIVE_TO + T_APPROACH so the swallow lands exactly as the
// approach transition begins — the wordmark rushes in ~2s into the video.
const S_LOGOTYPE = 64; // ~22f approach + ~1s hold + 12f fade out
const S_HERO = 176;
const F_STATION = 116; // each feature station
const T_WHIP = 12;
const S_FEATURES = F_STATION * 3 - T_WHIP * 2;
const S_POWERED = 160;
const S_COMMAND = 96;
const S_LOCKUP = 150;
const T_APPROACH = 22; // the wordmark rushes to meet the camera — short, not a long hold
const T_FADE = 12;
const T_PUSH = 22; // Command → Features focus-pull — long enough to read the rack
const T_FEAT = 14;
const T_GRAIN = 34; // Hero → Command grain wash — long enough to ease in, not pop
export const INTRODUCING_NEXTJS_DURATION =
S_HOOK +
S_ECLIPSE +
S_LOGOTYPE +
S_HERO +
S_FEATURES +
S_POWERED +
S_COMMAND +
S_LOCKUP -
(T_FADE + T_APPROACH + T_FADE + T_GRAIN + T_PUSH + T_FEAT + T_FADE);
// ---------------------------------------------------------------------------
// Small shared pieces
// ---------------------------------------------------------------------------
// Barely-there push-in so no frame is ever static.
const Drift: React.FC<{ children: React.ReactNode; grow?: number }> = ({
children,
grow = 0.03,
}) => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const scale = interpolate(frame, [0, durationInFrames], [1, 1 + grow]);
return (
<AbsoluteFill style={{ transform: `scale(${scale})` }}>
{children}
</AbsoluteFill>
);
};
// One continuous camera over the whole piece — a slow forward creep plus a
// faint sway, driven by the composition's ABSOLUTE frame, so the "camera"
// never resets at a cut. The transitions handle the per-scene handoffs; this
// is the connective tissue underneath them that keeps the video one take.
const CameraRig: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const t = frame / durationInFrames;
// Base >1 so the sway never reveals a frame edge; the creep is subtle by
// design — a big global zoom reads as nausea, a small one as intention.
const scale = 1.006 + t * 0.03 + Math.sin(frame * 0.012) * 0.004;
const ty = Math.sin(frame * 0.008) * 3;
return (
<AbsoluteFill style={{ transform: `scale(${scale}) translateY(${ty}px)` }}>
{children}
</AbsoluteFill>
);
};
// The single light source of the flight. One palette, one mask — the eclipse
// and the logotype ride the SAME halo, so the triangle-portal cut between them
// is a continuous field of light, not two patched-in glows.
const HALO_COLORS = ["#3a3a3a", "#666666", "#9a9a9a"];
const GrainHalo: React.FC<{
cx: number;
cy: number;
r: number;
opacity?: number;
breathe?: boolean;
}> = ({ cx, cy, r, opacity = 0.9, breathe = false }) => {
const frame = useCurrentFrame();
const b = breathe ? 1 + Math.sin(frame * 0.06) * 0.05 : 1;
const mask =
"radial-gradient(circle, rgba(0,0,0,1) 8%, rgba(0,0,0,0.45) 33%, transparent 62%)";
return (
<div
style={{
position: "absolute",
left: cx - r,
top: cy - r,
width: r * 2,
height: r * 2,
opacity,
WebkitMaskImage: mask,
maskImage: mask,
overflow: "hidden",
pointerEvents: "none",
}}
>
<div
style={{
position: "absolute",
left: "50%",
top: "50%",
width: 1280,
height: 720,
transform: `translate(-50%, -50%) scale(${b})`,
}}
>
<ShaderGrainGradient
colors={HALO_COLORS}
colorBack={BG}
softness={0.6}
intensity={0.4}
noise={0.7}
speed={0.5}
scale={1.6}
/>
</div>
</div>
);
};
// Hook → Eclipse: no flat dissolve. The line pushes a touch further in and
// blows out while the eclipse is revealed BEHIND it, settling back from a
// slight over-scale — one camera pulling focus from the words to the image.
type EmptyProps = Record<string, never>;
const SoftDolly: React.FC<
TransitionPresentationComponentProps<EmptyProps>
> = ({ children, presentationProgress, presentationDirection }) => {
const p = presentationProgress;
if (presentationDirection === "exiting") {
return (
<AbsoluteFill
style={{
transform: `scale(${interpolate(p, [0, 1], [1, 1.12], {
...clampOpts,
easing: Easing.in(Easing.cubic),
})})`,
opacity: interpolate(p, [0.15, 0.7], [1, 0], clampOpts),
}}
>
{children}
</AbsoluteFill>
);
}
if (p >= 1) return <AbsoluteFill>{children}</AbsoluteFill>;
return (
<AbsoluteFill
style={{
transform: `scale(${interpolate(p, [0, 1], [1.08, 1], {
...clampOpts,
easing: easeSoft,
})})`,
opacity: interpolate(p, [0.2, 0.75], [0, 1], clampOpts),
}}
>
{children}
</AbsoluteFill>
);
};
const softDolly = (): TransitionPresentation<EmptyProps> => ({
component: SoftDolly,
props: {},
});
// Words rise out of blur onto the baseline.
const RiseWords: React.FC<{
text: string;
delay?: number;
stagger?: number;
}> = ({ text, delay = 0, stagger = 2.5 }) => {
const frame = useCurrentFrame();
const words = text.split(" ");
return (
<>
{words.map((word, i) => {
const p = interpolate(frame - delay - i * stagger, [0, 20], [0, 1], {
...clampOpts,
easing: easeSoft,
});
// Travel lands at ~60% — the eased tail clicks the word down the pixel
// grid one pixel every few frames; opacity/blur keep the full curve.
const py = interpolate(frame - delay - i * stagger, [0, 12], [0, 1], {
...clampOpts,
easing: easeSoft,
});
return (
<span
key={i}
style={{
display: "inline-block",
whiteSpace: "pre",
opacity: p,
transform: `translateY(${(1 - py) * 18}px)`,
filter: p < 1 ? `blur(${(1 - p) * 7}px)` : undefined,
}}
>
{word}
{i < words.length - 1 ? " " : ""}
</span>
);
})}
</>
);
};
const Triangle: React.FC<{ size: number; color: string }> = ({
size,
color,
}) => (
<svg
width={size}
height={size * 0.882}
viewBox="0 0 100 88.2"
style={{ display: "block" }}
>
<path d="M50 0 L100 88.2 L0 88.2 Z" fill={color} />
</svg>
);
// The official circle-N icon, kept for the lockup.
const N_DIAGONAL =
"M149.508 157.52L69.142 54H54V125.97H66.114V69.384L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z";
const N_BAR = "M115 54H127V126H115V54Z";
const NextMark: React.FC<{ size: number; idPrefix: string }> = ({
size,
idPrefix,
}) => (
<svg width={size} height={size} viewBox="0 0 180 180">
<defs>
<linearGradient
id={`${idPrefix}-d`}
x1="109"
y1="116.5"
x2="144.5"
y2="160.5"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
<linearGradient
id={`${idPrefix}-b`}
x1="121"
y1="54"
x2="120.8"
y2="106.9"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
<circle cx="90" cy="90" r="88" fill="#000000" />
<circle
cx="90"
cy="90"
r="88"
fill="none"
stroke="rgba(237,237,237,0.3)"
strokeWidth="1.5"
/>
<path d={N_BAR} fill={`url(#${idPrefix}-b)`} />
<path d={N_DIAGONAL} fill={`url(#${idPrefix}-d)`} />
</svg>
);
// ===========================================================================
// Scene 1 — The eclipse. The backlit triangle over the grain field, the hook
// beneath it, and then the camera dives INTO the triangle — its flat black
// swallows the frame and the ring tunnel is already there beneath.
// ===========================================================================
const ECLIPSE_CX = 640;
const ECLIPSE_CY = 322;
// The camera flies INTO the ▲: the eclipse scales up from the triangle center
// so its flat-black interior swallows the frame, finishing exactly as the
// logo-approach transition takes over (DIVE_TO === S_ECLIPSE − T_APPROACH).
const DIVE_FROM = 28;
const DIVE_TO = 46;
// ===========================================================================
// Scene 0 — The opening line. Alone on black, it SLAMS in: crashing down out
// of an over-scale and heavy blur to a hard settle, then a slow, pompous
// push-in holds the claim before the eclipse.
// ===========================================================================
const HookScene: React.FC = () => {
const frame = useCurrentFrame();
// Brutal impact — over-scale + heavy blur crash into place, a hard settle.
const slam = interpolate(frame, [0, 9, 15], [1.42, 0.985, 1], {
...clampOpts,
easing: Easing.out(Easing.exp),
});
const op = interpolate(frame, [0, 5], [0, 1], clampOpts);
// Pompous hold — a slow, majestic push-in after the impact lands.
const grand = interpolate(frame, [15, S_HOOK], [1, 1.05], clampOpts);
// A short decaying shake sells the crash.
const shake = Math.sin(frame * 2.1) * Math.max(0, 1 - frame / 9) * 4;
// Brutal exit — the line blasts outward and dissolves before the eclipse,
// so it never lingers over the triangle during the cut.
const exit = interpolate(frame, [S_HOOK - 16, S_HOOK - 2], [0, 1], {
...clampOpts,
easing: Easing.in(Easing.cubic),
});
const blur = interpolate(frame, [0, 11], [24, 0], clampOpts) + exit * 26;
return (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<div
style={{
fontFamily: SANS,
fontWeight: 600,
fontSize: 64,
color: BRIGHT,
textAlign: "center",
padding: "0 90px",
opacity: op * (1 - exit),
transform: `translateY(${shake}px) scale(${slam * grand * (1 + exit * 0.2)})`,
filter: blur > 0.4 ? `blur(${blur}px)` : undefined,
}}
>
You've already used Next.js today
</div>
</AbsoluteFill>
);
};
const EclipseScene: React.FC = () => {
const frame = useCurrentFrame();
const glowIn = interpolate(frame, [0, 14], [0, 1], {
...clampOpts,
easing: easeOut,
});
// Fly INTO the triangle — the flat black interior grows to swallow the frame,
// completing right as the approach transition hands over to the logotype.
const dive = interpolate(frame, [DIVE_FROM, DIVE_TO], [0, 1], {
...clampOpts,
easing: Easing.in(Easing.cubic),
});
return (Showing the first 400 of 1111 lines. View the full file on GitHub.