{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal-simulator",
  "title": "Terminal Simulator",
  "description": "Terminal window that types out commands and rolls old lines off the top.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/terminal-simulator/index.tsx",
      "content": "\"use client\";\n\nimport {\n  interpolate,\n  Sequence,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type TerminalLineType = \"command\" | \"log\" | \"success\" | \"error\";\n\nexport interface TerminalLine {\n  text: string;\n  type: TerminalLineType;\n  delay?: number;\n  /**\n   * Extra freeze-frame pause AFTER this line finishes typing, in frames.\n   * Use this to build dramatic tension before the next batch of logs.\n   * If omitted, lines whose text ends in \"...\" auto-freeze for 18 frames.\n   */\n  pause?: number;\n}\n\nexport interface TerminalSimulatorProps {\n  lines?: TerminalLine[];\n  prompt?: string;\n  title?: string;\n  background?: string;\n  chromeColor?: string;\n  fontSize?: number;\n  /**\n   * Reveal speed. Despite the name, the reveal is now CHUNKED — each\n   * `1 / charsPerFrame` frames bumps the cursor by `chunkSize` characters,\n   * so output appears in bursts instead of dripping char-by-char.\n   */\n  charsPerFrame?: number;\n  /** Characters revealed per step. */\n  chunkSize?: number;\n  speed?: number;\n  className?: string;\n}\n\nconst DEFAULT_LINES: TerminalLine[] = [\n  { text: \"npm run build\", type: \"command\", delay: 0 },\n  { text: \"Resolving dependencies...\", type: \"log\", delay: 6 },\n  { text: \"> remocn@1.0.0 build\", type: \"log\", delay: 4 },\n  { text: \"> next build\", type: \"log\", delay: 4 },\n  { text: \"Compiling...\", type: \"log\", delay: 12 },\n  { text: \"Compiled successfully in 4.2s\", type: \"success\", delay: 14 },\n  { text: \"Generating static pages (24/24)\", type: \"log\", delay: 10 },\n  { text: \"Build completed without errors\", type: \"success\", delay: 12 },\n];\n\nconst TYPE_COLORS: Record<TerminalLineType, string> = {\n  command: \"#fafafa\",\n  log: \"#a1a1aa\",\n  success: \"#22c55e\",\n  error: \"#ef4444\",\n};\n\n/** Auto freeze-frame heuristic: any line ending in \"...\" holds the camera. */\nfunction autoPause(line: TerminalLine): number {\n  if (line.pause !== undefined) return line.pause;\n  if (line.text.trimEnd().endsWith(\"...\")) return 18;\n  return 0;\n}\n\nexport function TerminalSimulator({\n  lines = DEFAULT_LINES,\n  prompt = \"$\",\n  title = \"~/projects/remocn\",\n  background = \"#0a0a0a\",\n  chromeColor = \"#1a1a1a\",\n  fontSize = 18,\n  charsPerFrame = 1,\n  chunkSize = 1,\n  speed = 1,\n  className,\n}: TerminalSimulatorProps) {\n  const frame = useCurrentFrame() * speed;\n  const { fps } = useVideoConfig();\n\n  const lineHeight = Math.round(fontSize * 1.6);\n  const visibleLines = 8;\n  const windowWidth = 900;\n  const windowHeight = 480;\n\n  // Compute cumulative start frames for each line, including auto/explicit\n  // pauses AFTER a line finishes typing.\n  const starts: number[] = [];\n  let acc = 10;\n  for (let i = 0; i < lines.length; i++) {\n    const delay = lines[i].delay ?? 8;\n    acc += delay;\n    starts.push(acc);\n    // Approximate typing duration (in frames) for chunked output.\n    const typingFrames = Math.ceil(\n      lines[i].text.length / (chunkSize * charsPerFrame),\n    );\n    acc += typingFrames + autoPause(lines[i]);\n  }\n\n  // STEP-FUNCTION scroll. Each overflowing line snaps the buffer up by\n  // exactly one lineHeight on the frame it begins. No interpolation, no\n  // easing — terminals do not glide.\n  let translateY = 0;\n  for (let i = visibleLines; i < lines.length; i++) {\n    if (frame >= starts[i]) {\n      translateY -= lineHeight;\n    }\n  }\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          width: windowWidth,\n          height: windowHeight,\n          background,\n          borderRadius: 12,\n          overflow: \"hidden\",\n          boxShadow:\n            \"0 30px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)\",\n          display: \"flex\",\n          flexDirection: \"column\",\n          fontFamily:\n            \"var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace\",\n        }}\n      >\n        {/* Chrome */}\n        <div\n          style={{\n            height: 40,\n            background: chromeColor,\n            display: \"flex\",\n            alignItems: \"center\",\n            padding: \"0 16px\",\n            gap: 8,\n            borderBottom: \"1px solid rgba(255,255,255,0.06)\",\n          }}\n        >\n          <Light color=\"#ff5f57\" />\n          <Light color=\"#febc2e\" />\n          <Light color=\"#28c840\" />\n          <div\n            style={{\n              flex: 1,\n              textAlign: \"center\",\n              color: \"#71717a\",\n              fontSize: 13,\n            }}\n          >\n            {title}\n          </div>\n        </div>\n\n        {/* Content */}\n        <div\n          style={{\n            flex: 1,\n            padding: 20,\n            overflow: \"hidden\",\n            position: \"relative\",\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: 20,\n              right: 20,\n              top: 20,\n              translate: `0 ${translateY}px`,\n            }}\n          >\n            {lines.map((line, index) => (\n              <Sequence\n                key={index}\n                from={Math.round(starts[index] / speed)}\n                layout=\"none\"\n              >\n                <TerminalLineRow\n                  line={line}\n                  prompt={prompt}\n                  fontSize={fontSize}\n                  lineHeight={lineHeight}\n                  charsPerFrame={charsPerFrame}\n                  chunkSize={chunkSize}\n                  fps={fps}\n                  speed={speed}\n                />\n              </Sequence>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Light({ color }: { color: string }) {\n  return (\n    <div\n      style={{\n        width: 12,\n        height: 12,\n        borderRadius: \"50%\",\n        background: color,\n        opacity: 0.85,\n      }}\n    />\n  );\n}\n\nfunction TerminalLineRow({\n  line,\n  prompt,\n  fontSize,\n  lineHeight,\n  charsPerFrame,\n  chunkSize,\n  fps,\n  speed,\n}: {\n  line: TerminalLine;\n  prompt: string;\n  fontSize: number;\n  lineHeight: number;\n  charsPerFrame: number;\n  chunkSize: number;\n  fps: number;\n  speed: number;\n}) {\n  const localFrame = useCurrentFrame() * speed;\n  const totalChars = line.text.length;\n\n  // Chunked reveal: Math.floor of an interpolated count, then snapped to the\n  // nearest multiple of `chunkSize`. This is what gives the bursty terminal\n  // feel — text doesn't drip, it lurches.\n  const linearRevealed = Math.floor(\n    interpolate(localFrame, [0, totalChars / charsPerFrame], [0, totalChars], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    }),\n  );\n  const revealed = Math.min(\n    totalChars,\n    Math.ceil(linearRevealed / chunkSize) * chunkSize,\n  );\n  const visible = line.text.substring(0, revealed);\n  const typingDone = revealed >= totalChars;\n  // 2 Hz blink at any framerate.\n  const cursorVisible = Math.floor((localFrame / fps) * 2) % 2 === 0;\n\n  return (\n    <div\n      style={{\n        height: lineHeight,\n        fontSize,\n        color: TYPE_COLORS[line.type],\n        display: \"flex\",\n        alignItems: \"center\",\n        whiteSpace: \"pre\",\n      }}\n    >\n      {line.type === \"command\" && (\n        <span style={{ color: \"#22c55e\", marginRight: 8 }}>{prompt}</span>\n      )}\n      <span>{visible}</span>\n      {!typingDone && cursorVisible && (\n        <span\n          style={{\n            display: \"inline-block\",\n            width: fontSize * 0.55,\n            height: fontSize,\n            background: TYPE_COLORS[line.type],\n            marginLeft: 2,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/terminal-simulator.tsx"
    }
  ],
  "type": "registry:component"
}
