import React from "react";
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
import { FONT_DISPLAY, FONT_BODY } from "./parts";
/* ═══════════════════════════════════════════════════════════════════════
REUSABLE MONO ASSETS — animated visual building blocks.
All monochrome flat, transform/opacity only (no blur/SVG filters → fast
software render). Each is a self-contained component sized to fit the
"asset zone" of a scene (typically placed above the caption card).
PITFALL (verified): a flex-row container whose children use `flex:1`
collapses to width 0 when it is itself a horizontal flex item (intrinsic
width = sum of flex-basis:0 items = 0). Always give root wrappers an
explicit width so assets render regardless of parent layout.
═══════════════════════════════════════════════════════════════════════ */
const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
/** Growing bar chart — editorial "growth" visual. */
export const ChartBars: React.FC<{ bars?: number; delay?: number }> = ({ bars = 5, delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const heights = [0.42, 0.66, 0.5, 0.82, 0.62, 0.92, 0.72, 0.55, 0.88, 0.6];
const MAX_H = 180;
return (
{Array.from({ length: bars }).map((_, i) => {
const h = heights[(i + Math.floor(delay)) % heights.length];
const grow = interpolate(frame, [delay + i * 3, delay + i * 3 + 12], [0.05, 1], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
});
const px = Math.max(2, Math.round(h * MAX_H * grow));
return (
);
})}
);
};
/** Line chart that draws itself left→right (clip-width animation). */
export const ChartLine: React.FC<{ delay?: number }> = ({ delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = interpolate(frame, [delay, delay + fps * 0.9], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
return (
);
};
/** Ring/donut progress (percent). */
export const RingProgress: React.FC<{ pct?: number; size?: number; delay?: number }> = ({ pct = 0.72, size = 220, delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 20, stiffness: 60 } });
const r = 80;
const c = 2 * Math.PI * r;
const filled = Math.max(0, Math.min(1, pct * p));
return (
);
};
/** Gauge with sweeping needle. */
export const Gauge: React.FC<{ value?: number; label?: string; delay?: number }> = ({ value = 0.62, label = "ACCURACY", delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const v = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 16, stiffness: 70 } });
const angle = -90 + 180 * value * v; // -90°..+90°
return (
);
};
/** Scrolling ticker tape (endless marquee text). */
export const TickerTape: React.FC<{ text: string; speed?: number }> = ({ text, speed = 90 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const t = frame / fps;
const offset = (t * speed) % 50; // percent
const doubled = `${text} • ${text} • ${text}`;
return (
);
};
/** Big countdown / counter (rolling digits). */
export const Countdown: React.FC<{ end?: number; from?: number; delay?: number }> = ({ end = 3, from = 5, delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const t = (frame - delay) / fps;
const val = Math.max(end, Math.round(from - t));
return (
);
};
/** Node graph + data pulse traveling an edge (AI/workflow vibe). */
export const NodeFlow: React.FC<{ delay?: number }> = ({ delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const t = (frame - delay) / fps;
const pulse = (t % 1.4) / 1.4;
const nodes = [
{ x: 60, y: 160 },
{ x: 200, y: 60 },
{ x: 200, y: 260 },
{ x: 350, y: 160 },
];
const edges = [
[0, 1],
[0, 2],
[1, 3],
[2, 3],
];
return (
);
};
/** Pulse bars — audio-equalizer energy. */
export const PulseBars: React.FC<{ bars?: number; delay?: number }> = ({ bars = 12, delay = 0 }) => {
const frame = useCurrentFrame();
const t = (frame - delay) / 60;
return (
{Array.from({ length: bars }).map((_, i) => {
const base = 0.25 + 0.3 * Math.abs(Math.sin(i * 1.7 + t * 4));
return (
);
})}
);
};
/** Animated checklist — boxes tick one by one. */
export const Checklist: React.FC<{ items?: string[]; delay?: number }> = ({ items = ["PLAN", "BUILD", "SHIP"], delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
{items.map((it, i) => {
const p = spring({ frame: Math.max(0, frame - delay - i * 14), fps, config: { damping: 16, stiffness: 100 } });
const done = p > 0.35;
return (
);
})}
);
};
/** HUD corner frame + crosshair center (tech overlay asset). */
export const HudFrame: React.FC<{ label?: string }> = ({ label = "SYSTEM" }) => {
const frame = useCurrentFrame();
const t = frame / 60;
const c = 36;
const gap = 26;
return (
);
};
/** Percent stat block — big value + thin bar (editorial). */
export const StatBlock: React.FC<{ value: string; sub: string; delay?: number }> = ({ value, sub, delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const pop = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 14, stiffness: 80 } });
return (
);
};