+ {/* Title bar */}
+
+
+ {/* Terminal body */}
+
+ {renderedLines.map((line, idx) => {
+ if (line.isCmd) {
+ // Char-by-char typed command
+ const progress = interpolate(
+ frame,
+ [line.startFrame, line.endFrame],
+ [0, line.text.length],
+ { extrapolateRight: "clamp" }
+ );
+ const typed = line.text.slice(0, Math.floor(progress));
+ const isLatest = idx === renderedLines.length - 1;
+ const isActive = frame <= line.endFrame + fps * 0.2;
+ return (
+
+ {prompt}
+ {typed}
+ {isLatest && isActive && blinkPhase && (
+
+ )}
+
+ );
+ } else {
+ // Instant-reveal output line with fade-in
+ const alpha = interpolate(
+ frame,
+ [line.startFrame, line.endFrame],
+ [0, 1],
+ { extrapolateRight: "clamp" }
+ );
+ return (
+
+ {line.text}
+
+ );
+ }
+ })}
+
+
+ {/* Floating command pills */}
+ {pills
+ .filter(p => frame >= p.startFrame && frame <= p.endFrame)
+ .map((pill, idx) => {
+ const lifeProgress = (frame - pill.startFrame) / Math.max(1, pill.endFrame - pill.startFrame);
+ // spring in (0 → 1), hold, spring out (0.8 → 1.0)
+ const inAlpha = spring({
+ frame: frame - pill.startFrame,
+ fps,
+ config: { damping: 14, stiffness: 180 },
+ durationInFrames: Math.ceil(fps * 0.35),
+ });
+ const outAlpha =
+ lifeProgress > 0.82
+ ? interpolate(lifeProgress, [0.82, 1], [1, 0], { extrapolateRight: "clamp" })
+ : 1;
+ const alpha = Math.min(inAlpha, outAlpha);
+ const translateY = interpolate(inAlpha, [0, 1], [14, 0]);
+ return (
+
+ {pill.text}
+
+ );
+ })}
+
+