diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx
index d43ed70b..a136b6cc 100644
--- a/apps/videos/src/Root.tsx
+++ b/apps/videos/src/Root.tsx
@@ -15,8 +15,10 @@ import { PipelineFlow } from "./compositions/features/PipelineFlow";
import { OneCommand } from "./compositions/hero/OneCommand";
import { PrivacyPromise } from "./compositions/hero/PrivacyPromise";
import { ToolGalaxy } from "./compositions/hero/ToolGalaxy";
+import { ProductDemo } from "./compositions/product-demo/ProductDemo";
import { PromoTeaser } from "./compositions/promo-teaser/PromoTeaser";
import { PromoTeaserVertical } from "./compositions/promo-teaser/PromoTeaserVertical";
+import { The48 } from "./compositions/showcase/The48";
import { XLaunchVideo } from "./compositions/x-launch/XLaunchVideo";
import { TEXT } from "./lib/fonts";
@@ -155,5 +157,21 @@ export const RemotionRoot: React.FC = () => (
width={1080}
height={1080}
/>
+
+
>
);
diff --git a/apps/videos/src/compositions/showcase/The48.tsx b/apps/videos/src/compositions/showcase/The48.tsx
new file mode 100644
index 00000000..3e1c1090
--- /dev/null
+++ b/apps/videos/src/compositions/showcase/The48.tsx
@@ -0,0 +1,641 @@
+import { Trail } from "@remotion/motion-blur";
+import type React from "react";
+import {
+ AbsoluteFill,
+ interpolate,
+ Sequence,
+ spring,
+ useCurrentFrame,
+ useVideoConfig,
+} from "remotion";
+import { ClipReveal } from "@/components/ClipReveal";
+import { GrainOverlay } from "@/components/GrainOverlay";
+import { ToolPill } from "@/components/ToolPill";
+import { CATEGORY_LABELS, CATEGORY_ORDER, COLOR } from "@/lib/colors";
+import { TEXT } from "@/lib/fonts";
+import { EASE, SPRING } from "@/lib/motion";
+import { getToolsByCategory } from "@/lib/tools";
+
+/* ------------------------------------------------------------------ */
+/* Grid layout constants (from spec) */
+/* ------------------------------------------------------------------ */
+
+const COLS = 8;
+const ROWS = 6;
+const CELL_W = 190;
+const CELL_H = 44;
+const GAP = 8;
+const GRID_W = COLS * CELL_W + (COLS - 1) * GAP; // 1576
+const GRID_H = ROWS * CELL_H + (ROWS - 1) * GAP; // 304
+const GRID_X = (1920 - GRID_W) / 2;
+const GRID_Y = (1080 - GRID_H) / 2 + 40;
+
+const CX = 1920 / 2;
+const CY = 1080 / 2;
+const FRAMES_PER_TOOL = 15;
+
+/* ------------------------------------------------------------------ */
+/* Pre-compute tool order and grid positions */
+/* ------------------------------------------------------------------ */
+
+interface ToolEntry {
+ index: number;
+ name: string;
+ category: string;
+ col: number;
+ row: number;
+ gridX: number;
+ gridY: number;
+}
+
+const TOOL_ENTRIES: ToolEntry[] = [];
+const colCounts: number[] = new Array(COLS).fill(0);
+
+for (const cat of CATEGORY_ORDER) {
+ const colIdx = CATEGORY_ORDER.indexOf(cat);
+ const tools = getToolsByCategory(cat);
+ for (const tool of tools) {
+ const row = colCounts[colIdx];
+ colCounts[colIdx] = row + 1;
+ TOOL_ENTRIES.push({
+ index: TOOL_ENTRIES.length,
+ name: tool.name,
+ category: tool.category,
+ col: colIdx,
+ row,
+ gridX: GRID_X + colIdx * (CELL_W + GAP) + CELL_W / 2,
+ gridY: GRID_Y + row * (CELL_H + GAP) + CELL_H / 2,
+ });
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Micro-visualization categories */
+/* ------------------------------------------------------------------ */
+
+type MicroViz = "scale" | "clip" | "rotate" | "sparkle" | "squeeze" | "stamp" | "badge" | "pulse";
+
+function getMicroViz(name: string, category: string): MicroViz {
+ const lower = name.toLowerCase();
+ if (lower.includes("resize") || lower.includes("upscal")) return "scale";
+ if (lower.includes("crop") || lower.includes("split")) return "clip";
+ if (lower.includes("rotate") || lower.includes("flip")) return "rotate";
+ if (
+ lower.includes("convert") ||
+ lower.includes("svg") ||
+ lower.includes("pdf") ||
+ lower.includes("gif")
+ )
+ return "badge";
+ if (lower.includes("compress") || lower.includes("optimi")) return "squeeze";
+ if (lower.includes("watermark") || lower.includes("stamp") || lower.includes("overlay"))
+ return "stamp";
+ if (category === "ai") return "sparkle";
+ return "pulse";
+}
+
+/* ------------------------------------------------------------------ */
+/* Entry direction helper */
+/* ------------------------------------------------------------------ */
+
+type Direction = "left" | "right" | "top";
+
+function getEntryDirection(index: number): Direction {
+ if (index % 8 === 0 && index > 0) return "top";
+ return index % 2 === 0 ? "left" : "right";
+}
+
+function getEntryPosition(dir: Direction): { x: number; y: number } {
+ switch (dir) {
+ case "left":
+ return { x: -200, y: CY };
+ case "right":
+ return { x: 1920 + 200, y: CY };
+ case "top":
+ return { x: CX, y: -80 };
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Sparkle dots component */
+/* ------------------------------------------------------------------ */
+
+const SparkleDots: React.FC<{ progress: number }> = ({ progress }) => {
+ const offsets = [
+ { dx: -16, dy: -16 },
+ { dx: 16, dy: -12 },
+ { dx: -12, dy: 16 },
+ { dx: 18, dy: 14 },
+ ];
+ const spread = interpolate(progress, [0, 1], [0, 1]);
+ const opacity = interpolate(progress, [0, 0.3, 1], [0, 1, 0], {
+ extrapolateRight: "clamp",
+ });
+
+ return (
+ <>
+ {offsets.map((o) => (
+
+ ))}
+ >
+ );
+};
+
+/* ------------------------------------------------------------------ */
+/* AnimatedToolPill -- self-contained 15-frame animation */
+/* ------------------------------------------------------------------ */
+
+const AnimatedToolPill: React.FC<{ tool: ToolEntry }> = ({ tool }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+
+ const dir = getEntryDirection(tool.index);
+ const entry = getEntryPosition(dir);
+ const viz = getMicroViz(tool.name, tool.category);
+
+ // Phase 1: entrance (frame 0-3) -- edge to center
+ // Phase 2: center flash (frame 3-5) -- micro-visualization
+ // Phase 3: grid snap (frame 5-15) -- center to grid position
+
+ let x: number;
+ let y: number;
+ let extraTransform = "";
+ let clipPath: string | undefined;
+ let sparkleProgress = 0;
+ let showSparkle = false;
+
+ if (frame < 3) {
+ // Entrance: edge to center
+ const t = interpolate(frame, [0, 3], [0, 1], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ easing: EASE.snap,
+ });
+ x = entry.x + (CX - entry.x) * t;
+ y = entry.y + (CY - entry.y) * t;
+ } else if (frame < 5) {
+ // Center flash with micro-visualization
+ x = CX;
+ y = CY;
+ const vizProgress = interpolate(frame, [3, 5], [0, 1], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ });
+
+ switch (viz) {
+ case "scale": {
+ const s = interpolate(vizProgress, [0, 0.5, 1], [1.3, 0.8, 1.0]);
+ extraTransform = ` scale(${s})`;
+ break;
+ }
+ case "clip": {
+ const inset = interpolate(vizProgress, [0, 0.5, 1], [0, 15, 0]);
+ clipPath = `inset(${inset}% ${inset}% ${inset}% ${inset}%)`;
+ break;
+ }
+ case "rotate": {
+ const deg = interpolate(vizProgress, [0, 1], [0, 90]);
+ extraTransform = ` rotate(${deg}deg)`;
+ break;
+ }
+ case "badge": {
+ const s = interpolate(vizProgress, [0, 0.3, 0.7, 1], [0.9, 1.15, 1.15, 1.0]);
+ extraTransform = ` scale(${s})`;
+ break;
+ }
+ case "squeeze": {
+ const sx = interpolate(vizProgress, [0, 0.5, 1], [1, 0.7, 1]);
+ const sy = interpolate(vizProgress, [0, 0.5, 1], [1, 1.2, 1]);
+ extraTransform = ` scaleX(${sx}) scaleY(${sy})`;
+ break;
+ }
+ case "stamp": {
+ const ty = interpolate(vizProgress, [0, 0.4, 0.6, 1], [-20, 4, -2, 0]);
+ const s = interpolate(vizProgress, [0, 0.4, 0.6, 1], [1.2, 0.95, 1.02, 1]);
+ extraTransform = ` translateY(${ty}px) scale(${s})`;
+ break;
+ }
+ case "sparkle": {
+ showSparkle = true;
+ sparkleProgress = vizProgress;
+ const s = interpolate(vizProgress, [0, 0.5, 1], [1.0, 1.1, 1.0]);
+ extraTransform = ` scale(${s})`;
+ break;
+ }
+ default: {
+ const s = interpolate(vizProgress, [0, 0.5, 1], [1.0, 1.1, 1.0]);
+ extraTransform = ` scale(${s})`;
+ break;
+ }
+ }
+ } else {
+ // Grid snap: center to grid position via spring
+ const s = spring({
+ frame: frame - 5,
+ fps,
+ config: SPRING.settle,
+ });
+ x = CX + (tool.gridX - CX) * s;
+ y = CY + (tool.gridY - CY) * s;
+ }
+
+ return (
+
+
+ {showSparkle && }
+
+ );
+};
+
+/* ------------------------------------------------------------------ */
+/* Entrance phase wrapper -- uses Trail for motion blur frames 0-3 */
+/* ------------------------------------------------------------------ */
+
+const ToolEntrance: React.FC<{ tool: ToolEntry }> = ({ tool }) => {
+ const frame = useCurrentFrame();
+ const inMotion = frame < 5;
+
+ if (inMotion) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return ;
+};
+
+/* ------------------------------------------------------------------ */
+/* Running counter (top-right, amber) */
+/* ------------------------------------------------------------------ */
+
+const RunningCounter: React.FC = () => {
+ const frame = useCurrentFrame();
+
+ // Counter shows how many tools have landed (reached frame 15 in their local sequence)
+ // Tool i starts at frame 30 + i*15, lands at frame 30 + i*15 + 15 = 45 + i*15
+ let landed = 0;
+ for (let i = 0; i < TOOL_ENTRIES.length; i++) {
+ const landFrame = 30 + i * FRAMES_PER_TOOL + FRAMES_PER_TOOL;
+ if (frame >= landFrame) landed = i + 1;
+ }
+
+ // Don't show counter before any tool has started
+ if (frame < 45) return null;
+
+ // During Act 3 (frame 770-810), counter transitions to "48 tools" and moves to center-top
+ const isAct3Text = frame >= 770;
+
+ // Fade in
+ const opacity = interpolate(frame, [45, 50], [0, 1], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ });
+
+ if (isAct3Text) {
+ const moveProgress = interpolate(frame, [770, 800], [0, 1], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ easing: EASE.enter,
+ });
+ const xPos = interpolate(moveProgress, [0, 1], [1920 - 100, CX]);
+ const yPos = interpolate(moveProgress, [0, 1], [40, 30]);
+
+ return (
+
+
+ 48 tools
+
+
+ );
+ }
+
+ return (
+
+
+ {landed}
+
+
+ );
+};
+
+/* ------------------------------------------------------------------ */
+/* Grid glow wave (Act 3, frame 750-770) */
+/* ------------------------------------------------------------------ */
+
+const GridGlowWave: React.FC = () => {
+ const frame = useCurrentFrame();
+
+ if (frame < 750 || frame > 780) return null;
+
+ return (
+ <>
+ {CATEGORY_ORDER.map((cat, colIdx) => {
+ const glowStart = 750 + colIdx * 3;
+ const glowOpacity = interpolate(
+ frame,
+ [glowStart, glowStart + 6, glowStart + 12],
+ [0, 0.2, 0],
+ {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ },
+ );
+
+ const colX = GRID_X + colIdx * (CELL_W + GAP);
+
+ return (
+
+ );
+ })}
+ >
+ );
+};
+
+/* ------------------------------------------------------------------ */
+/* Category labels (Act 3, frame 770-810) */
+/* ------------------------------------------------------------------ */
+
+const CategoryLabelsRow: React.FC = () => {
+ const frame = useCurrentFrame();
+
+ if (frame < 770) return null;
+
+ // Act 4 fade
+ const act4Opacity = interpolate(frame, [810, 820], [1, 0.6], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ });
+
+ return (
+ <>
+ {CATEGORY_ORDER.map((cat, i) => {
+ const labelX = GRID_X + i * (CELL_W + GAP) + CELL_W / 2;
+ const labelY = GRID_Y - 24;
+ const catColor = COLOR.category[cat] ?? COLOR.accent;
+
+ return (
+
+
+
+ {CATEGORY_LABELS[cat]}
+
+
+
+ );
+ })}
+ >
+ );
+};
+
+/* ------------------------------------------------------------------ */
+/* Main composition */
+/* ------------------------------------------------------------------ */
+
+export const The48: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+
+ /* ================================================================ */
+ /* Act 1: Title (frame 0-30) */
+ /* ================================================================ */
+
+ const titleScale =
+ frame < 30
+ ? interpolate(spring({ frame, fps, config: SPRING.settle }), [0, 1], [1.1, 1.0])
+ : 1.0;
+
+ // Title moves to top and shrinks during frames 30-60
+ const titleY = interpolate(frame, [0, 30, 60], [CY - 48, CY - 48, 60], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ easing: EASE.enter,
+ });
+
+ const titleFontSize = interpolate(frame, [30, 60], [96, 48], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ easing: EASE.enter,
+ });
+
+ // Title as persistent header after frame 30 (fades down to subtle)
+ const headerOpacity =
+ frame >= 30
+ ? interpolate(frame, [30, 60], [1, 0.7], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ })
+ : 0;
+
+ // During Act 4, header fades out
+ const headerAct4Opacity =
+ frame >= 810
+ ? interpolate(frame, [810, 820], [0.7, 0], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ })
+ : headerOpacity;
+
+ /* ================================================================ */
+ /* Act 3: Grid pulse (frame 750-770) */
+ /* ================================================================ */
+
+ const gridPulseScale =
+ frame >= 750 && frame < 770
+ ? 1 +
+ 0.015 *
+ Math.sin(
+ interpolate(frame, [750, 770], [0, Math.PI], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ }),
+ )
+ : 1.0;
+
+ /* ================================================================ */
+ /* Act 4: Grid fades, tagline (frame 810-840) */
+ /* ================================================================ */
+
+ const gridOpacity =
+ frame >= 810
+ ? interpolate(frame, [810, 825], [1, 0.6], {
+ extrapolateLeft: "clamp",
+ extrapolateRight: "clamp",
+ })
+ : 1;
+
+ return (
+
+ {/* ---- Act 1: Title ---- */}
+ {frame < 30 && (
+
+
+
+ 48 tools.
+
+
+
+ )}
+
+ {/* ---- Title as header (frame 30+) ---- */}
+ {frame >= 30 && frame < 820 && (
+
+
+ 48 tools.
+
+
+ )}
+
+ {/* ---- Act 2: Rapid montage ---- */}
+
+ {TOOL_ENTRIES.map((tool) => (
+
+
+
+ ))}
+
+
+ {/* ---- Grid glow wave (Act 3) ---- */}
+
+
+ {/* ---- Category labels (Act 3) ---- */}
+
+
+
+
+ {/* ---- Running counter ---- */}
+
+
+ {/* ---- Act 4: Tagline ---- */}
+ {frame >= 810 && (
+
+
+
+ One container. Zero cloud.
+
+
+
+ )}
+
+
+
+ );
+};