{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal-simulator",
  "title": "Terminal Simulator",
  "description": "Terminal window with chunked command playback, freeze-frame pauses, step scrolling, and an optional cursor-pinned zoom camera.",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/terminal-simulator/index.tsx",
      "content": "\"use client\";\n\nimport {\n  Easing,\n  getRemotionEnvironment,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport {\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n  withAlpha,\n} from \"@/lib/snap-cn-ui\";\n\nexport type TerminalLineType = \"command\" | \"log\" | \"success\" | \"error\";\n\n/** Package-manager command panel — the flythrough's second station. */\nexport interface CommandSpec {\n  /** Tab labels. The first is the active tab. */\n  managers?: string[];\n  /** The command typed into the active tab, e.g. \"npm install One Tool\". */\n  text: string;\n}\n\nexport interface TerminalLine {\n  text: string;\n  type: TerminalLineType;\n  /** Frame gap before this line starts typing. Defaults to 8. */\n  delay?: number;\n  /**\n   * Extra freeze-frame pause AFTER this line finishes typing, in frames.\n   * Use this to build tension before the next batch of logs. If omitted,\n   * lines whose text ends in \"...\" auto-freeze for 18 frames.\n   */\n  pause?: number;\n}\n\nexport interface TerminalZoom {\n  /** Turn the extra landing zoom on. */\n  enabled: boolean;\n  /** Camera zoom the final punch-in lands at. Defaults to 2.4. */\n  scale?: number;\n  /** Kept for API compatibility; the flythrough camera always leads the eye. */\n  followCursor?: boolean;\n}\n\nexport interface TerminalSimulatorProps {\n  lines?: TerminalLine[];\n  /**\n   * Headline at the first camera station. Wrap words in `*asterisks*` to tint\n   * them with the accent gradient. `null`/\"\" skips the station — the camera\n   * starts on the command panel instead.\n   */\n  intro?: string | null;\n  /**\n   * Command panel at the second camera station. `null` skips the station.\n   */\n  command?: CommandSpec | null;\n  /** Command-panel surface color. */\n  /** Overrides the design system's `card`. */\n  background?: string;\n  /** Hairline border color for the command-panel ring and divider. */\n  /** Overrides the design system's `border`. */\n  borderColor?: string;\n  /** Design-system token overrides. */\n  theme?: Partial<SnapCnTheme>;\n  /** Defaults to `\"dark\"` — a terminal is lit for dark. */\n  mode?: \"light\" | \"dark\";\n  fontSize?: number;\n  /**\n   * Reveal speed. The reveal is CHUNKED — every `1 / charsPerFrame` frames\n   * bumps the cursor by `chunkSize` characters, so output appears in bursts\n   * instead of dripping char-by-char.\n   */\n  charsPerFrame?: number;\n  /** Characters revealed per step. */\n  chunkSize?: number;\n  /**\n   * Landing zoom of the final punch onto the terminal. Pass `true` for the\n   * 2.4x default or an object for a custom scale. Off = 1.5x.\n   */\n  zoom?: boolean | TerminalZoom;\n  speed?: number;\n  className?: string;\n}\n\n// ─── Stage & camera constants ────────────────────────────────────────────────\n// The scene is ONE world the camera flies over. Nothing enters from a screen\n// edge — the camera pans/zooms between three fixed stations on a single black\n// canvas, exactly like a camera-type-flythrough.\n\n// The stage, the panel and the tab row are app chrome and come from the design\n// system. The syntax palette below is the terminal's own colour scheme — the\n// look, not chrome — and stays literal, exposed through props.\n\n/** Station anchors in world px (world is sized for a 1280x720 viewport). */\nconst INTRO_POS = { x: 640, y: 360 };\nconst CMD_POS = { x: 2140, y: 520 };\nconst TERM_POS = { x: 2140, y: 1660 };\n\n/** Camera timeline (frames at speed 1). */\nconst DRIFT_START = 22; // intro starts creeping left\nconst WHIP_START = 40; // sudden acceleration toward the command panel\nconst CMD_ARRIVE = 70; // camera lands on the command panel\nconst CMD_TYPE_START = 74; // \"npm install One Tool\" starts typing\nconst PUSH_END = 112; // slow push-in while typing (leaves the ghost trail)\nconst PULL_END = 138; // zoom-out + descend toward the terminal\nconst PUNCH_END = 158; // sudden punch-in lands on the terminal\nconst TERM_TYPE_START = 146; // terminal lines start typing as we land\n\n/** Command-panel geometry in world px. */\nconst CMD_W = 760;\nconst CMD_H = 170;\n\n/** Terminal station geometry in world px (sized against the reference). */\nconst TERM_BAR_OFFSET = 137; // bar sits this far left of the focus point\nconst TERM_TEXT_OFFSET = 90; // text column left edge relative to focus\nconst TERM_BLOCK_TOP = 215; // first line sits this far above the focus point\nconst TERM_LINE_H = 83;\nconst TERM_FONT = 44;\n\n/** Lead-in used for the terminal lines' start schedule. */\nconst LEAD_IN_FRAMES = 10;\n\n// ─── Palette (matched against the reference stills) ─────────────────────────\n\n/** Accent gradient for `*emphasized*` intro words. */\nconst ACCENT_GRADIENT = \"linear-gradient(90deg, #7C96FF 0%, #A78BFA 100%)\";\n\n/** Command-row syntax palette: dim manager/subcommand, highlighted args. */\nconst CMD_MANAGER_COLOR = \"#A0673F\";\nconst CMD_SUBCOMMAND_COLOR = \"#64789B\";\nconst CMD_ARG_COLOR = \"#D7E4F8\";\n\n/** Focus-terminal palette. */\nconst TERM_ACCENT = \"#2EA043\";\nconst TERM_CURSOR = \"#1D5B36\";\nconst TERM_COLORS: Record<TerminalLineType, string> = {\n  command: \"#F2F5F3\",\n  log: \"#6E7873\",\n  success: \"#1F8039\",\n  error: \"#F04438\",\n};\n\n/** Functional color map kept for the public helper API. */\nconst TYPE_COLORS: Record<TerminalLineType, string> = TERM_COLORS;\n\nconst SANS_FAMILY =\n  'var(--font-geist-sans), -apple-system, BlinkMacSystemFont, \"Segoe UI\", Inter, sans-serif';\nconst MONO_FAMILY =\n  \"var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace\";\n\nconst DEFAULT_INTRO = \"*work* one step at a *time.*\";\n\nconst DEFAULT_COMMAND: CommandSpec = {\n  managers: [\"npm\", \"pnpm\", \"yarn\", \"bun\"],\n  text: \"npm install One Tool\",\n};\n\nconst DEFAULT_LINES: TerminalLine[] = [\n  { text: \"{labels: ['bug']})\", type: \"log\", delay: 0 },\n  { text: \"→   label added\", type: \"success\", delay: 6 },\n  { text: \"$\", type: \"log\", delay: 10 },\n  { text: \"One task\", type: \"command\", delay: 4 },\n];\n\n// ─── Pure helpers ────────────────────────────────────────────────────────────\n\n/** Auto freeze-frame heuristic: any line ending in \"...\" holds the camera. */\nexport function 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\n/** Frames the chunked reveal needs to type `textLength` characters. */\nexport function typingDuration(\n  textLength: number,\n  charsPerFrame: number,\n  chunkSize: number,\n): number {\n  return Math.ceil(textLength / (chunkSize * charsPerFrame));\n}\n\n/**\n * Cumulative start frame for each line, including per-line delays and the\n * auto/explicit freeze pauses AFTER a line finishes typing.\n */\nexport function computeLineStarts(\n  lines: TerminalLine[],\n  charsPerFrame: number,\n  chunkSize: number,\n  leadIn: number = LEAD_IN_FRAMES,\n): number[] {\n  const starts: number[] = [];\n  let acc = leadIn;\n  for (const line of lines) {\n    acc += line.delay ?? 8;\n    starts.push(acc);\n    acc +=\n      typingDuration(line.text.length, charsPerFrame, chunkSize) +\n      autoPause(line);\n  }\n  return starts;\n}\n\n/**\n * Chunked reveal: Math.floor of an interpolated count, snapped up to the\n * nearest multiple of `chunkSize`. This is what gives the bursty terminal\n * feel — text doesn't drip, it lurches.\n */\nexport function chunkedReveal(\n  localFrame: number,\n  totalChars: number,\n  charsPerFrame: number,\n  chunkSize: number,\n): number {\n  if (totalChars <= 0) return 0;\n  const linear = Math.floor(\n    interpolate(localFrame, [0, totalChars / charsPerFrame], [0, totalChars], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    }),\n  );\n  return Math.min(totalChars, Math.ceil(linear / chunkSize) * chunkSize);\n}\n\n/**\n * STEP-FUNCTION scroll. Each overflowing line snaps the buffer up by exactly\n * one row on the frame it begins. No interpolation, no easing — terminals do\n * not glide. Returns the number of rows scrolled off the top.\n */\nexport function computeScrollRows(\n  frame: number,\n  starts: number[],\n  visibleLines: number,\n): number {\n  let rows = 0;\n  for (let i = visibleLines; i < starts.length; i++) {\n    if (frame >= starts[i]) rows++;\n  }\n  return rows;\n}\n\n/** Normalize the `zoom` prop into a fully-resolved camera config. */\nexport function resolveZoom(\n  zoom: boolean | TerminalZoom | undefined,\n): Required<TerminalZoom> {\n  if (zoom === undefined || zoom === false) {\n    return { enabled: false, scale: 1, followCursor: true };\n  }\n  if (zoom === true) {\n    return { enabled: true, scale: 2.4, followCursor: true };\n  }\n  return {\n    enabled: zoom.enabled,\n    scale: zoom.scale ?? 2.4,\n    followCursor: zoom.followCursor ?? true,\n  };\n}\n\n/** Split `*emphasis*` runs out of the intro string, preserving spaces. */\nexport function parseIntro(\n  text: string,\n): Array<{ text: string; accent: boolean }> {\n  const out: Array<{ text: string; accent: boolean }> = [];\n  const re = /\\*([^*]+)\\*/g;\n  let last = 0;\n  let m: RegExpExecArray | null = re.exec(text);\n  while (m !== null) {\n    if (m.index > last)\n      out.push({ text: text.slice(last, m.index), accent: false });\n    out.push({ text: m[1], accent: true });\n    last = m.index + m[0].length;\n    m = re.exec(text);\n  }\n  if (last < text.length) out.push({ text: text.slice(last), accent: false });\n  return out;\n}\n\n/** Colour a shell command: word 0 = manager, word 1 = subcommand, rest = args. */\nexport function commandSpans(\n  text: string,\n): Array<{ text: string; color: string }> {\n  const out: Array<{ text: string; color: string }> = [];\n  let wordIndex = -1;\n  for (const part of text.split(/(\\s+)/)) {\n    if (part.length === 0) continue;\n    if (/^\\s+$/.test(part)) {\n      out.push({ text: part, color: CMD_ARG_COLOR });\n      continue;\n    }\n    wordIndex++;\n    const color =\n      wordIndex === 0\n        ? CMD_MANAGER_COLOR\n        : wordIndex === 1\n          ? CMD_SUBCOMMAND_COLOR\n          : CMD_ARG_COLOR;\n    out.push({ text: part, color });\n  }\n  return out;\n}\n\n// ─── Camera ──────────────────────────────────────────────────────────────────\n\nexport interface CameraStop {\n  /** Frame this stop is reached. */\n  frame: number;\n  /** World focus point (px) the camera centers on. */\n  x: number;\n  y: number;\n  /** Zoom at this stop. */\n  z: number;\n  /** Easing applied while travelling INTO this stop from the previous one. */\n  easing?: (t: number) => number;\n}\n\nexport interface CameraState {\n  x: number;\n  y: number;\n  z: number;\n}\n\n/**\n * Piecewise camera resolver. Between consecutive stops the camera lerps\n * x/y/z with the segment's own easing — so a lazy drift can hand off to a\n * sudden ease-in whip, which is the whole feel of the flythrough. Clamps to\n * the first/last stop outside the timeline.\n */\nexport function cameraAt(frame: number, stops: CameraStop[]): CameraState {\n  const first = stops[0];\n  const last = stops[stops.length - 1];\n  if (frame <= first.frame) return { x: first.x, y: first.y, z: first.z };\n  if (frame >= last.frame) return { x: last.x, y: last.y, z: last.z };\n  for (let i = 0; i < stops.length - 1; i++) {\n    const a = stops[i];\n    const b = stops[i + 1];\n    if (frame >= a.frame && frame < b.frame) {\n      const t = (frame - a.frame) / (b.frame - a.frame);\n      const e = b.easing ? b.easing(t) : t;\n      return {\n        x: a.x + (b.x - a.x) * e,\n        y: a.y + (b.y - a.y) * e,\n        z: a.z + (b.z - a.z) * e,\n      };\n    }\n  }\n  return { x: last.x, y: last.y, z: last.z };\n}\n\n/**\n * The flythrough's shot list: hold on the intro → creep left → sudden\n * ease-in whip to the command panel → slow push while it types (this is\n * what draws the ghost trail) → zoom-out descending → sudden punch-in\n * landing on the terminal. When a station is skipped its segment collapses.\n */\nexport function buildCameraStops(\n  hasIntro: boolean,\n  hasCommand: boolean,\n  landingZoom: number,\n): CameraStop[] {\n  const whip = Easing.bezier(0.7, 0, 0.25, 1);\n  const punch = Easing.bezier(0.75, 0, 0.3, 1);\n  const glide = Easing.bezier(0.35, 0, 0.55, 1);\n\n  const stops: CameraStop[] = [];\n  if (hasIntro) {\n    stops.push(\n      { frame: 0, x: INTRO_POS.x, y: INTRO_POS.y, z: 1 },\n      { frame: DRIFT_START, x: INTRO_POS.x, y: INTRO_POS.y, z: 1 },\n      // The slight leftward text drift = camera creeping right, accelerating.\n      {\n        frame: WHIP_START,\n        x: INTRO_POS.x + 52,\n        y: INTRO_POS.y,\n        z: 1.02,\n        easing: Easing.in(Easing.quad),\n      },\n    );\n  }\n  if (hasCommand) {\n    stops.push(\n      {\n        frame: hasIntro ? CMD_ARRIVE : 0,\n        x: CMD_POS.x,\n        y: CMD_POS.y,\n        z: 1.55,\n        easing: whip,\n      },\n      // Slow push-in while the command types — leaves the trailing copies.\n      {\n        frame: PUSH_END,\n        x: CMD_POS.x,\n        y: CMD_POS.y + 28,\n        z: 1.9,\n        easing: Easing.inOut(Easing.quad),\n      },\n      // Zoom out and start descending toward the terminal.\n      {\n        frame: PULL_END,\n        x: CMD_POS.x,\n        y: CMD_POS.y + 630,\n        z: 0.95,\n        easing: glide,\n      },\n    );\n  }\n  stops.push({\n    frame: hasCommand ? PUNCH_END : hasIntro ? CMD_ARRIVE : 0,\n    x: TERM_POS.x,\n    y: TERM_POS.y,\n    z: landingZoom,\n    easing: punch,\n  });\n  return stops;\n}\n\n/** Screen-space speed of the focus point — drives the motion-blur trail. */\nexport function cameraSpeed(frame: number, stops: CameraStop[]): number {\n  const a = cameraAt(frame - 1, stops);\n  const b = cameraAt(frame, stops);\n  const pan = Math.hypot(b.x - a.x, b.y - a.y) * b.z;\n  const zoomEdge = Math.abs(b.z - a.z) * 360;\n  return pan + zoomEdge;\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\n/** Ghost samples for the motion-blur trail: frames back, opacity, blur px. */\nconst GHOSTS = [\n  { back: 2, alpha: 0.3, blur: 3 },\n  { back: 4, alpha: 0.18, blur: 6 },\n  { back: 6, alpha: 0.1, blur: 10 },\n];\n\nexport function TerminalSimulator({\n  lines = DEFAULT_LINES,\n  intro = DEFAULT_INTRO,\n  command = DEFAULT_COMMAND,\n  background,\n  borderColor,\n  theme,\n  mode,\n  fontSize = 18,\n  charsPerFrame = 2,\n  chunkSize = 3,\n  zoom = false,\n  speed = 1,\n  className,\n}: TerminalSimulatorProps) {\n  const frame = useCurrentFrame() * speed;\n  const { fps, width, height } = useVideoConfig();\n  const { isRendering } = getRemotionEnvironment();\n  const t = useSnapCnTheme(theme, mode ?? \"dark\");\n  const panel = background ?? t.card;\n  const hairline = borderColor ?? t.border;\n  // One constant stage across the whole flythrough — the page, a shade under\n  // the panel that floats on it.\n  const stage = mixOklch(t.background, \"#000\", 0.35);\n\n  const hasIntro = intro != null && intro.trim().length > 0;\n  const hasCommand = command != null && command.text.trim().length > 0;\n  const camera = resolveZoom(zoom);\n  const landingZoom = camera.enabled ? camera.scale : 1.5;\n\n  const stops = buildCameraStops(hasIntro, hasCommand, landingZoom);\n  const cam = cameraAt(frame, stops);\n  const speedPx = cameraSpeed(frame, stops);\n  // Trail strength ramps in from barely-moving; saturates on the whip.\n  const trail = Math.min(1, Math.max(0, (speedPx - 2) / 30));\n\n  const worldTransform = (c: CameraState) =>\n    `translate(${width / 2 - c.x * c.z}px, ${height / 2 - c.y * c.z}px) scale(${c.z})`;\n\n  const world = (\n    <WorldContent\n      frame={frame}\n      fps={fps}\n      intro={hasIntro ? intro : null}\n      command={hasCommand ? command : null}\n      lines={lines}\n      background={panel}\n      borderColor={hairline}\n      ink={t.foreground}\n      inkMuted={t.mutedForeground}\n      fontSize={fontSize}\n      charsPerFrame={charsPerFrame}\n      chunkSize={chunkSize}\n      hasIntro={hasIntro}\n    />\n  );\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        background: stage,\n        textRendering: \"geometricPrecision\",\n      }}\n    >\n      {/* Motion-blur trail: the same world drawn at recent camera positions.\n          Only alive while the camera actually moves — it IS the drop of the\n          fast moves, no element ever slides in from a screen edge. */}\n      {trail > 0.01 &&\n        GHOSTS.map((g) => (\n          <div\n            key={g.back}\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              transformOrigin: \"0 0\",\n              transform: worldTransform(cameraAt(frame - g.back, stops)),\n              opacity: g.alpha * trail,\n              filter: `blur(${g.blur * trail}px)`,\n              pointerEvents: \"none\",\n            }}\n          >\n            {world}\n          </div>\n        ))}\n\n      {/* The camera */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          transformOrigin: \"0 0\",\n          transform: worldTransform(cam),\n          ...(isRendering ? {} : { willChange: \"transform\" }),\n        }}\n      >\n        {world}\n      </div>\n    </div>\n  );\n}\n\n// ─── World (all three stations on one canvas) ────────────────────────────────\n\nfunction WorldContent({\n  frame,\n  fps,\n  intro,\n  command,\n  lines,\n  background,\n  borderColor,\n  ink,\n  inkMuted,\n  fontSize,\n  charsPerFrame,\n  chunkSize,\n  hasIntro,\n}: {\n  frame: number;\n  fps: number;\n  intro: string | null;\n  command: CommandSpec | null;\n  lines: TerminalLine[];\n  background: string;\n  borderColor: string;\n  ink: string;\n  inkMuted: string;\n  fontSize: number;\n  charsPerFrame: number;\n  chunkSize: number;\n  hasIntro: boolean;\n}) {\n  const s = fontSize / 18; // font scale, layout stays fixed\n\n  return (\n    <div style={{ position: \"absolute\", left: 0, top: 0 }}>\n      {intro !== null && (\n        <IntroStation frame={frame} text={intro} fontScale={s} ink={ink} />\n      )}\n      {command !== null && (\n        <CommandStation\n          frame={frame}\n          command={command}\n          background={background}\n          borderColor={borderColor}\n          ink={ink}\n          inkMuted={inkMuted}\n          fontScale={s}\n          charsPerFrame={charsPerFrame}\n          chunkSize={chunkSize}\n          fps={fps}\n          typeStart={hasIntro ? CMD_TYPE_START : 8}\n        />\n      )}\n      <TerminalStation\n        frame={frame}\n        lines={lines}\n        fontScale={s}\n        charsPerFrame={charsPerFrame}\n        chunkSize={chunkSize}\n        fps={fps}\n        typeStart={\n          command !== null ? TERM_TYPE_START : hasIntro ? CMD_ARRIVE - 6 : 6\n        }\n      />\n    </div>\n  );\n}\n\n/** Station 1 — the headline, words fading in staggered. */\nfunction IntroStation({\n  frame,\n  text,\n  fontScale,\n  ink,\n}: {\n  frame: number;\n  text: string;\n  fontScale: number;\n  ink: string;\n}) {\n  const tokens = parseIntro(text);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: INTRO_POS.x,\n        top: INTRO_POS.y,\n        transform: \"translate(-50%, -50%)\",\n        whiteSpace: \"pre\",\n        fontFamily: SANS_FAMILY,\n        fontSize: Math.round(58 * fontScale),\n        fontWeight: 600,\n        letterSpacing: \"-0.01em\",\n      }}\n    >\n      {tokens.map((tok, i) => {\n        const t = interpolate(frame, [2 + i * 4, 14 + i * 4], [0, 1], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n          easing: Easing.out(Easing.cubic),\n        });\n        const style = tok.accent\n          ? {\n              backgroundImage: ACCENT_GRADIENT,\n              WebkitBackgroundClip: \"text\" as const,\n              backgroundClip: \"text\" as const,\n              color: \"transparent\",\n            }\n          : { color: ink };\n        return (\n          <span\n            // biome-ignore lint/suspicious/noArrayIndexKey: positional tokens\n            key={i}\n            style={{ ...style, opacity: t, filter: `blur(${(1 - t) * 3}px)` }}\n          >\n            {tok.text}\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\n/** Station 2 — the package-manager panel with the highlighted install command. */\nfunction CommandStation({\n  frame,\n  command,\n  background,\n  borderColor,\n  ink,\n  inkMuted,\n  fontScale,\n  charsPerFrame,\n  chunkSize,\n  fps,\n  typeStart,\n}: {\n  frame: number;\n  command: CommandSpec;\n  background: string;\n  borderColor: string;\n  ink: string;\n  inkMuted: string;\n  fontScale: number;\n  charsPerFrame: number;\n  chunkSize: number;\n  fps: number;\n  typeStart: number;\n}) {\n  const managers = command.managers?.length ? command.managers : [\"npm\"];\n  const revealed = chunkedReveal(\n    Math.max(0, frame - typeStart),\n    command.text.length,\n    charsPerFrame,\n    chunkSize,\n  );\n  const typingDone = revealed >= command.text.length;\n  const blink = Math.floor((frame / fps) * 2) % 2 === 0;\n  const spans = commandSpans(command.text);\n\n  const tabFont = Math.round(20 * fontScale);\n  const rowFont = Math.round(33 * fontScale);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: CMD_POS.x - CMD_W / 2,\n        top: CMD_POS.y - CMD_H / 2,\n        width: CMD_W,\n        height: CMD_H,\n        background,\n        borderRadius: 14,\n        overflow: \"hidden\",\n        boxShadow: `0 0 0 1px ${borderColor}, 0 30px 60px ${withAlpha(\n          mixOklch(background, \"#000\", 0.8),\n          0.55,\n        )}`,\n        fontFamily: MONO_FAMILY,\n      }}\n    >\n      {/* Tab row */}\n      <div\n        style={{\n          height: 54,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 6,\n          padding: \"0 16px\",\n          borderBottom: `1px solid ${borderColor}`,\n        }}\n      >\n        {managers.map((m, i) => (\n          <div\n            key={m}\n            style={{\n              fontSize: tabFont,\n              fontWeight: i === 0 ? 600 : 500,\n              padding: \"7px 14px\",\n              borderRadius: 8,\n              color: i === 0 ? ink : inkMuted,\n              background: i === 0 ? withAlpha(ink, 0.1) : \"transparent\",\n            }}\n          >\n            {m}\n          </div>\n        ))}\n      </div>\n\n      {/* Command row — manager and subcommand dim, the args highlighted */}\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          height: CMD_H - 54,\n          padding: \"0 24px\",\n          fontSize: rowFont,\n          letterSpacing: \"0.01em\",\n          whiteSpace: \"pre\",\n        }}\n      >\n        {(() => {\n          let acc = 0;\n          return spans.map((p, i) => {\n            const start = acc;\n            acc += p.text.length;\n            const vis = Math.max(0, Math.min(p.text.length, revealed - start));\n            return (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: positional spans\n                key={i}\n                style={{ color: p.color }}\n              >\n                {p.text.slice(0, vis)}\n              </span>\n            );\n          });\n        })()}\n        {!typingDone && frame >= typeStart && blink && (\n          <span\n            style={{\n              display: \"inline-block\",\n              width: rowFont * 0.55,\n              height: rowFont,\n              background: CMD_ARG_COLOR,\n              marginLeft: 2,\n            }}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\n/** Station 3 — the green focus terminal. */\nfunction TerminalStation({\n  frame,\n  lines,\n  fontScale,\n  charsPerFrame,\n  chunkSize,\n  fps,\n  typeStart,\n}: {\n  frame: number;\n  lines: TerminalLine[];\n  fontScale: number;\n  charsPerFrame: number;\n  chunkSize: number;\n  fps: number;\n  typeStart: number;\n}) {\n  const starts = computeLineStarts(lines, charsPerFrame, chunkSize, typeStart);\n  const font = Math.round(TERM_FONT * fontScale);\n  const blink = Math.floor((frame / fps) * 2) % 2 === 0;\n\n  return (\n    <>\n      {/* Accent bar */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: TERM_POS.x - TERM_BAR_OFFSET,\n          top: TERM_POS.y - 520,\n          width: 5,\n          height: 1040,\n          background: TERM_ACCENT,\n          boxShadow: `0 0 22px ${withAlpha(TERM_ACCENT, 0.75)}`,\n        }}\n      />\n      {/* Output column */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: TERM_POS.x - TERM_TEXT_OFFSET,\n          top: TERM_POS.y - TERM_BLOCK_TOP,\n          fontFamily: MONO_FAMILY,\n        }}\n      >\n        {lines.map((line, index) => {\n          const revealed = chunkedReveal(\n            Math.max(0, frame - starts[index]),\n            line.text.length,\n            charsPerFrame,\n            chunkSize,\n          );\n          const typingDone = revealed >= line.text.length;\n          const isLast = index === lines.length - 1;\n          const showCursor =\n            line.type === \"command\" &&\n            frame >= starts[index] &&\n            (!typingDone || (isLast && blink));\n          return (\n            <div\n              // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional and never reorder\n              key={index}\n              style={{\n                height: TERM_LINE_H,\n                display: \"flex\",\n                alignItems: \"center\",\n                color: TERM_COLORS[line.type],\n                fontSize: font,\n                whiteSpace: \"pre\",\n              }}\n            >\n              <span>{line.text.substring(0, revealed)}</span>\n              {showCursor && (\n                <span\n                  style={{\n                    display: \"inline-block\",\n                    width: font * 0.6,\n                    height: font * 0.95,\n                    background: TERM_CURSOR,\n                    marginLeft: 6,\n                  }}\n                />\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </>\n  );\n}\n\nexport { TYPE_COLORS };\n",
      "type": "registry:component",
      "target": "components/snap-cn/terminal-simulator.tsx"
    }
  ],
  "type": "registry:component"
}