Catalog

Edit Props (live)

TEXT
TEXT
config
config
config
config
config
config
config
config
config
config
config
config

Source

345 lines
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  Easing,
  spring,
} from "remotion";
import { loadFont as loadMono } from "@remotion/google-fonts/JetBrainsMono";

loadMono("normal", { weights: ["400", "500", "700"] });

const MONO = "'JetBrains Mono', monospace";
const ACCENT = "#D97757";

type State = "welcome" | "typing" | "executing";

interface ResultLine {
  text: string;
  // Optional inline highlight ranges, e.g. highlight a substring orange.
  highlights?: { from: number; to: number }[];
}

interface ClaudeCodeTerminalProps {
  // Background tone — orange-peach for "warm" intro shots, cream for product cards.
  backgroundColor?: string;
  // Window
  windowTitle?: string;        // tab text, e.g. "Claude Code" or "• Refactor PickleCoach to Ne…"
  // Welcome header
  welcomeName?: string;        // "Nick"
  modelInfo?: string;          // "Sonnet 4.6 with medium effort"
  planLabel?: string;          // "Claude Pro"
  cwd?: string;                // "C:\\Users\\shubh"
  // State
  terminalState?: State;       // "welcome" (default) | "typing" | "executing"
  // typing state
  command?: string;            // "/model opus"
  // executing state — shows command result block + spinner
  resultLines?: ResultLine[];  // [{text:"/model opusplan"}, {text:"└ Set model to Opus 4.6 …"}]
  toolCall?: string;           // "Explore(Explore PickleCoach project structure)"
  toolDetail?: string;         // "└ Bash(ls -la …)"
  statusText?: string;         // "Sautéing… (18s · ↓ 269 tokens)"
  showInterruptHint?: boolean; // "esc to interrupt"
  // typing speed
  typingCps?: number;          // default 30
}

const isLight = (hex: string) => {
  const h = hex.replace("#", "");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return (r * 0.299 + g * 0.587 + b * 0.114) > 180;
};

// Pixel monster mascot — 8x6 grid, drawn as colored square cells.
const MONSTER_PIXELS = [
  "..XXXXXX..",
  ".XXXXXXXX.",
  "XX.XX.XXXX",
  "XXXXXXXXXX",
  "X.XX.X.XXX",
  "X........X",
];

const PixelMonster: React.FC<{ size?: number; color?: string }> = ({ size = 90, color = ACCENT }) => {
  const cell = Math.floor(size / 10);
  return (
    <div style={{ display: "inline-block" }}>
      {MONSTER_PIXELS.map((row, r) => (
        <div key={r} style={{ display: "flex" }}>
          {row.split("").map((c, x) => (
            <div
              key={x}
              style={{
                width: cell, height: cell,
                background: c === "X" ? color : "transparent",
              }}
            />
          ))}
        </div>
      ))}
    </div>
  );
};

export const ClaudeCodeTerminal: React.FC<ClaudeCodeTerminalProps> = ({
  backgroundColor = "#E89070",
  windowTitle = "Claude Code",
  welcomeName = "there",
  modelInfo = "Sonnet 4.6 with medium effort",
  planLabel = "Claude Pro",
  cwd = "C:\\Users\\shubh",
  terminalState = "welcome",
  command = "/model opus",
  resultLines,
  toolCall,
  toolDetail,
  statusText,
  showInterruptHint,
  typingCps = 30,
}) => {
  const frame = useCurrentFrame();
  const { fps, width, height, durationInFrames } = useVideoConfig();

  // Window dims — square-ish centered panel
  const winW = Math.round(width * 0.86);
  const winH = Math.round(height * 0.46);
  const winLeft = Math.round((width - winW) / 2);
  const winTop = Math.round(height * 0.20);

  const chromeOp = interpolate(frame / fps, [0, 0.25], [0, 1], { extrapolateRight: "clamp" });
  const chromeScale = interpolate(frame / fps, [0, 0.25], [0.97, 1], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Typing animation for `command`
  const typeStart = Math.round(D(durationInFrames, 0.12));
  const typeChars = Math.max(0, Math.floor(((frame - typeStart) / fps) * typingCps));
  const typedCmd = command.slice(0, typeChars);
  const cmdCaret = typeChars < command.length && Math.floor(frame / 9) % 2 === 0;

  // Spinner glyphs for executing state
  const SPINNER = ["·", "•", "◦", "•"];
  const spin = SPINNER[Math.floor(frame / 6) % SPINNER.length];

  const headerColor = isLight(backgroundColor) ? "#1a1a1a" : "#ffffff";

  return (
    <AbsoluteFill style={{ backgroundColor, overflow: "hidden", fontFamily: MONO }}>
      {/* Window */}
      <div
        style={{
          position: "absolute",
          left: winLeft, top: winTop,
          width: winW, height: winH,
          background: "#0d0d0f",
          borderRadius: 12,
          overflow: "hidden",
          boxShadow: "0 30px 80px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.06)",
          opacity: chromeOp,
          transform: `scale(${chromeScale})`,
          transformOrigin: "center center",
          display: "flex",
          flexDirection: "column",
        }}
      >
        {/* Windows-style title bar (matches reference reels — not mac dots) */}
        <div
          style={{
            height: 38,
            background: "#1a1a1c",
            borderBottom: "1px solid rgba(255,255,255,0.06)",
            display: "flex",
            alignItems: "center",
            paddingLeft: 8,
            paddingRight: 0,
            flexShrink: 0,
          }}
        >
          {/* Browser-shield icon */}
          <span style={{ width: 28, height: 28, display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#777", fontSize: 18 }}>◉</span>
          {/* Folder icon */}
          <span style={{ width: 28, height: 28, display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#777", fontSize: 16 }}>▦</span>
          {/* Active tab */}
          <div
            style={{
              display: "flex", alignItems: "center", gap: 8,
              background: "#0d0d0f",
              borderTop: `2px solid ${ACCENT}`,
              padding: "0 12px",
              height: "100%",
              maxWidth: 280,
            }}
          >
            <span style={{ color: ACCENT, fontSize: 14 }}>▣</span>
            <span style={{ color: "#d4d4d4", fontSize: 14, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
              {windowTitle}
            </span>
            <span style={{ color: "#666", fontSize: 14, marginLeft: 4 }}>×</span>
          </div>
          {/* New tab + dropdown */}
          <span style={{ marginLeft: 8, color: "#666", fontSize: 18 }}>+</span>
          <span style={{ marginLeft: 6, color: "#666", fontSize: 14 }}>⌄</span>
          <div style={{ flex: 1 }} />
          <span style={{ color: "#666", fontSize: 18, padding: "0 12px" }}>—</span>
          <span style={{ color: "#666", fontSize: 14, padding: "0 12px" }}>▢</span>
          <span style={{ color: "#666", fontSize: 18, padding: "0 14px" }}>×</span>
        </div>

        {/* Body */}
        <div
          style={{
            flex: 1,
            padding: "16px 20px",
            color: "#d4d4d4",
            fontFamily: MONO,
            fontSize: 18,
            lineHeight: 1.5,
            overflow: "hidden",
            display: "flex",
            flexDirection: "column",
            minHeight: 0,
          }}
        >
          {/* Welcome banner — bordered rounded box */}
          <div
            style={{
              border: `1px solid ${ACCENT}55`,
              borderRadius: 10,
              padding: "14px 18px",
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              gap: 10,
              flexShrink: 0,
            }}
          >
            <div style={{ position: "relative", width: "100%" }}>
              <div style={{ position: "absolute", left: -2, top: -22, color: ACCENT, fontSize: 14, background: "#0d0d0f", padding: "0 8px" }}>
                Claude Code
              </div>
            </div>
            <div style={{ color: "#ffffff", fontSize: 22, fontWeight: 500 }}>Welcome back {welcomeName}!</div>
            <PixelMonster size={80} />
            <div style={{ color: "#bdbdbd", fontSize: 16, textAlign: "center", lineHeight: 1.5 }}>
              {modelInfo}<br />
              {planLabel}<br />
              {cwd}
            </div>
          </div>

          {/* Body content varies by state */}
          {terminalState === "welcome" && (
            <div style={{ flex: 1, marginTop: 18 }}>
              <PromptLine text="" caret={Math.floor(frame / 9) % 2 === 0} />
            </div>
          )}

          {terminalState === "typing" && (
            <div style={{ flex: 1, marginTop: 18 }}>
              <PromptLine text={typedCmd} caret={cmdCaret} />
            </div>
          )}

          {terminalState === "executing" && (
            <div style={{ flex: 1, marginTop: 14, display: "flex", flexDirection: "column", gap: 6 }}>
              {/* Result lines (e.g. /model opusplan + tree) */}
              {(resultLines || []).map((rl, i) => (
                <div key={i} style={{ color: i === 0 ? "#ffffff" : "#d4d4d4", whiteSpace: "pre-wrap" }}>
                  {renderHighlighted(rl)}
                </div>
              ))}
              {/* Tool call block */}
              {toolCall && (
                <div style={{ color: "#ffffff", whiteSpace: "pre-wrap" }}>{toolCall}</div>
              )}
              {toolDetail && (
                <div style={{ color: "#bdbdbd", whiteSpace: "pre-wrap" }}>{toolDetail}</div>
              )}
              {/* Status / spinner */}
              {statusText && (
                <div style={{ marginTop: 6, color: "#d4d4d4", display: "flex", alignItems: "center", gap: 8 }}>
                  <span style={{ color: ACCENT }}>{spin}</span>
                  <span>{statusText}</span>
                </div>
              )}
              <div style={{ flex: 1 }} />
              {/* Empty prompt at bottom + interrupt hint */}
              <PromptLine text="" caret={Math.floor(frame / 9) % 2 === 0} />
              {showInterruptHint && (
                <div style={{ color: "#777", fontSize: 14 }}>esc to interrupt</div>
              )}
            </div>
          )}
        </div>
      </div>

      {/* Watermark-y caption left blank — kinetic captions ride on top globally */}
      {/* (no extra label — keep the surface clean for the reel's caption band) */}
      <div style={{ display: "none" }} aria-hidden>
        {/* dim, mostly to silence unused-vars in headerColor for now */}
        {headerColor}
      </div>
    </AbsoluteFill>
  );
};

const PromptLine: React.FC<{ text: string; caret: boolean }> = ({ text, caret }) => (
  <div
    style={{
      borderTop: "1px solid rgba(255,255,255,0.10)",
      borderBottom: "1px solid rgba(255,255,255,0.10)",
      padding: "10px 0",
      display: "flex",
      alignItems: "center",
      gap: 12,
      color: "#d4d4d4",
    }}
  >
    <span style={{ color: ACCENT, fontWeight: 700, fontSize: 20 }}>{">"}</span>
    <span style={{ minHeight: 22 }}>
      {text}
      {caret && (
        <span
          style={{
            display: "inline-block",
            width: 10,
            height: 22,
            background: "#d4d4d4",
            verticalAlign: "text-bottom",
            marginLeft: 2,
          }}
        />
      )}
    </span>
  </div>
);

function renderHighlighted(rl: ResultLine): React.ReactNode {
  const { text, highlights } = rl;
  if (!highlights || !highlights.length) return text;
  // Sort + emit alternating plain/highlight spans
  const sorted = [...highlights].sort((a, b) => a.from - b.from);
  const out: React.ReactNode[] = [];
  let cursor = 0;
  sorted.forEach((h, i) => {
    if (h.from > cursor) out.push(text.slice(cursor, h.from));
    out.push(
      <span key={i} style={{ background: ACCENT, color: "#fff", padding: "0 4px", borderRadius: 2 }}>
        {text.slice(h.from, h.to)}
      </span>,
    );
    cursor = h.to;
  });
  if (cursor < text.length) out.push(text.slice(cursor));
  return out;
}

function D(durFrames: number, frac: number): number {
  return Math.round(durFrames * frac);
}

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: ClaudeCodeTerminal
// 14 props detected · 2 port(s)

Claude Code Terminal

claude_code_terminal

Claude Code Terminal — Andy's hand-crafted Remotion scene. 14 props. windowTitle:TEXT, statusText:TEXT.

Workflow Inputs (2)

  • Window Title
    windowTitle
    TEXT
  • Status Text
    statusText
    TEXT

Config Fields (12)

  • cwd
  • command
  • toolCall
  • modelInfo
  • planLabel
  • typingCps
  • toolDetail
  • resultLines
  • welcomeName
  • terminalState
  • backgroundColor
  • showInterruptHint

Meta

Updated
4/21/2026