← Catalog
Edit Props (live)
TEXT
config
config
config
config
config
config
Source
341 linesimport React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { loadFont as loadMono } from "@remotion/google-fonts/JetBrainsMono";
loadInter("normal", { weights: ["400", "700", "900"] });
loadMono("normal", { weights: ["400", "500", "700"] });
type LineKind = "cmd" | "out" | "err" | "ok" | "comment" | "blank";
interface TerminalLine {
text: string;
kind?: LineKind;
// Override typing speed for this line
cps?: number;
}
interface TerminalTypingProps {
lines: TerminalLine[];
title?: string;
backgroundColor?: string;
// Default typing speed chars-per-sec
defaultCps?: number;
// Flavour: "mac", "linux", "ohnie"
flavour?: "mac" | "linux" | "ohnie";
prompt?: string;
// Scale of terminal font
fontSize?: number;
}
const MONO = "'JetBrains Mono', monospace";
const FONT = "'Inter', sans-serif";
// Simple tokenizer to colorize command lines
function renderCommandTokens(text: string): React.ReactNode {
const tokens = text.split(/(\s+|['"][^'"]*['"]|\$\w+|--?[\w-]+)/).filter(Boolean);
return tokens.map((tok, i) => {
if (/^['"]/.test(tok)) {
return <span key={i} style={{ color: "#a8d98a" }}>{tok}</span>;
}
if (/^--?[\w-]+$/.test(tok)) {
return <span key={i} style={{ color: "#ffb080" }}>{tok}</span>;
}
if (/^\$\w+/.test(tok)) {
return <span key={i} style={{ color: "#c4a7ff" }}>{tok}</span>;
}
if (i === 0 && /\w/.test(tok)) {
return <span key={i} style={{ color: "#57ceb2", fontWeight: 500 }}>{tok}</span>;
}
return <span key={i}>{tok}</span>;
});
}
export const TerminalTyping: React.FC<TerminalTypingProps> = ({
lines,
title,
backgroundColor = "#0a0612",
defaultCps = 28,
flavour = "mac",
prompt = "~",
fontSize = 32,
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const t = frame / fps;
// Compute per-line start/duration in seconds
const lineMeta: { startT: number; endT: number; chars: number }[] = [];
let cursor = 0.4; // small chrome-in delay
for (const line of lines) {
const cps = line.cps ?? defaultCps;
const chars = line.text.length;
const isCmd = (line.kind ?? "cmd") === "cmd";
const dur = isCmd ? chars / cps : 0.25; // command: type, output: instant
lineMeta.push({ startT: cursor, endT: cursor + dur, chars });
cursor += dur + (isCmd ? 0.35 : 0.15);
}
// Terminal chrome fade-in
const chromeOp = interpolate(t, [0, 0.35], [0, 1], { extrapolateRight: "clamp" });
const chromeScale = interpolate(t, [0, 0.35], [0.96, 1], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Terminal box layout — anchored below title, height grows to fit content
const { width, height } = useVideoConfig();
const termW = width - 100;
const lineH = fontSize * 1.55;
// chrome bar + inner padding + N lines + trailing prompt + bottom buffer
const visibleLineCount = lines.length + 1;
const termContentH = 52 + 60 + visibleLineCount * lineH + 80;
const termX = 50;
const termY = title ? 220 : 140;
// Fill remaining vertical space up to a sensible max
const termH = Math.min(height - termY - 120, Math.max(900, termContentH));
// Cursor blink
const cursorBlink = Math.floor(t * 2) % 2 === 0;
// Title reveal
const titleReveal = interpolate(frame, [0, 20], [100, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Scroll-up when content overflows
const maxVisibleLines = Math.floor((termH - 60) / lineH);
return (
<AbsoluteFill
style={{
backgroundColor,
fontFamily: FONT,
overflow: "hidden",
}}
>
{/* Soft glow backdrop */}
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse at 50% 40%, rgba(87,206,178,0.12), transparent 60%)",
pointerEvents: "none",
}}
/>
{/* Title */}
{title && (
<div
style={{
position: "absolute",
top: 100,
left: 60,
right: 60,
fontSize: 54,
fontWeight: 900,
color: "#fff",
letterSpacing: -1.2,
clipPath: `inset(0 ${titleReveal}% 0 0)`,
WebkitClipPath: `inset(0 ${titleReveal}% 0 0)`,
}}
>
{title}
</div>
)}
{/* Terminal window */}
<div
style={{
position: "absolute",
left: termX,
top: termY,
width: termW,
height: termH,
background: "#12101a",
borderRadius: 22,
border: "1px solid rgba(255,255,255,0.12)",
boxShadow: "0 30px 80px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.08)",
overflow: "hidden",
opacity: chromeOp,
transform: `scale(${chromeScale})`,
transformOrigin: "center center",
}}
>
{/* Title bar */}
<div
style={{
height: 52,
background: "linear-gradient(to bottom, #1f1a2a, #181424)",
borderBottom: "1px solid rgba(255,255,255,0.06)",
display: "flex",
alignItems: "center",
padding: "0 20px",
gap: 10,
}}
>
{flavour !== "ohnie" && (
<>
<div style={{ width: 14, height: 14, borderRadius: 7, background: "#ff5f57" }} />
<div style={{ width: 14, height: 14, borderRadius: 7, background: "#febc2e" }} />
<div style={{ width: 14, height: 14, borderRadius: 7, background: "#28c840" }} />
</>
)}
{flavour === "ohnie" && (
<div
style={{
width: 22,
height: 22,
background: "linear-gradient(135deg, #57ceb2, #509adb)",
borderRadius: 5,
}}
/>
)}
<div
style={{
marginLeft: 14,
fontFamily: MONO,
fontSize: 20,
color: "rgba(255,255,255,0.55)",
fontWeight: 400,
}}
>
{flavour === "ohnie" ? "ohnie@reel" : flavour === "mac" ? "user@macbook-pro — -zsh" : "user@ubuntu:~"}
</div>
</div>
{/* Terminal body */}
<div
style={{
padding: "24px 30px",
fontFamily: MONO,
fontSize,
lineHeight: 1.55,
color: "#e8e2f5",
position: "relative",
height: "100%",
}}
>
{lines.map((line, idx) => {
const meta = lineMeta[idx];
if (!meta) return null;
if (t < meta.startT) return null;
const kind = line.kind ?? "cmd";
const isCmd = kind === "cmd";
// Character reveal for commands
let shown = line.text;
let showCaret = false;
if (isCmd) {
const charT = interpolate(t, [meta.startT, meta.endT], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const numChars = Math.floor(charT * line.text.length);
shown = line.text.slice(0, numChars);
showCaret = t < meta.endT + 0.15;
}
const colorMap: Record<LineKind, string> = {
cmd: "#ffffff",
out: "rgba(232,226,245,0.85)",
err: "#ff7a7a",
ok: "#57ceb2",
comment: "rgba(184,158,255,0.65)",
blank: "transparent",
};
// Compute scroll offset so latest line stays in view
const completedLines = lineMeta.filter((m) => t >= m.startT).length;
const scrollOff = Math.max(0, completedLines - maxVisibleLines) * lineH;
return (
<div
key={idx}
style={{
display: "flex",
alignItems: "baseline",
gap: 14,
color: colorMap[kind],
transform: `translateY(-${scrollOff}px)`,
transition: "transform 0.3s ease",
}}
>
{isCmd && (
<span style={{ color: "#57ceb2", flexShrink: 0 }}>
<span style={{ color: "rgba(87,206,178,0.65)" }}>{prompt}</span>{" "}
<span style={{ color: "#57ceb2", fontWeight: 500 }}>$</span>
</span>
)}
<span
style={{
whiteSpace: "pre",
wordBreak: "break-all",
}}
>
{isCmd ? renderCommandTokens(shown) : shown}
{showCaret && cursorBlink && (
<span
style={{
display: "inline-block",
width: fontSize * 0.5,
height: fontSize * 0.9,
background: "#57ceb2",
marginLeft: 2,
verticalAlign: "text-bottom",
}}
/>
)}
</span>
</div>
);
})}
{/* Final idle cursor after all lines */}
{(() => {
const allDone = lineMeta.length > 0 && t > lineMeta[lineMeta.length - 1].endT + 0.3;
if (!allDone) return null;
const completedLines = lineMeta.filter((m) => t >= m.startT).length;
const scrollOff = Math.max(0, completedLines - maxVisibleLines) * lineH;
return (
<div
style={{
display: "flex",
alignItems: "baseline",
gap: 14,
color: "#fff",
transform: `translateY(-${scrollOff}px)`,
}}
>
<span style={{ color: "rgba(87,206,178,0.65)" }}>{prompt}</span>{" "}
<span style={{ color: "#57ceb2", fontWeight: 500 }}>$</span>
{cursorBlink && (
<span
style={{
display: "inline-block",
width: fontSize * 0.5,
height: fontSize * 0.9,
background: "#57ceb2",
marginLeft: -6,
verticalAlign: "text-bottom",
}}
/>
)}
</div>
);
})()}
</div>
</div>
</AbsoluteFill>
);
};
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: TerminalTyping
// 7 props detected · 1 port(s)
Terminal Typing
terminal_typingTerminal Typing — Andy's hand-crafted Remotion scene. 7 props. title:TEXT.
Workflow Inputs (1)
- Title
titleTEXT
Config Fields (6)
linespromptflavourfontSizedefaultCpsbackgroundColor
Meta
- Updated
- 4/21/2026