{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "terminal",
  "title": "Terminal",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/terminal/terminal.component.ts",
      "content": "import {\n  type AfterViewInit,\n  Component,\n  ElementRef,\n  Input,\n  type OnDestroy,\n  ViewChild,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\nexport interface TerminalLine {\n  /** Prompt prefix shown before the command. Omit for output-only lines. */\n  prompt?: string\n  /** Command text shown after the prompt. */\n  command?: string\n  /** Output lines rendered below the command. */\n  output?: string\n  /** Override the line type: 'command' renders prompt+command, 'output' renders plain text. */\n  type?: 'command' | 'output'\n}\n\nexport interface ResolvedTerminalLine extends TerminalLine {\n  type: 'command' | 'output'\n  prompt: string\n}\n\n/**\n * Angular port of UIPKGE Terminal (React `Terminal`): macOS-style title bar with traffic-light\n * dots, command history with prompt + output, dark/light themes, auto-scroll to the newest\n * line, optional typing animation (one line per `typingSpeed` ms) and a max-height scroll.\n * Visible-line count is a signal so the timer-driven reveal renders in zoneless apps.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-terminal, [ui-terminal]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"terminal\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-theme]': 'theme',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div class=\"border-border bg-muted flex items-center gap-2 border-b px-4 py-2.5\">\n      <div class=\"flex gap-1.5\">\n        <span class=\"bg-destructive size-3 rounded-full\"></span>\n        <span class=\"bg-warning size-3 rounded-full\"></span>\n        <span class=\"bg-success size-3 rounded-full\"></span>\n      </div>\n      <span class=\"text-muted-foreground ml-2 text-xs\">{{ title }}</span>\n    </div>\n    <div #body class=\"overflow-auto p-4 leading-relaxed\" [style.max-height]=\"maxHeight\">\n      @for (line of shownLines(); track $index) {\n        <div data-slot=\"terminal-line\" class=\"break-words whitespace-pre-wrap\">\n          @if (line.type === 'command') {\n            <div data-slot=\"terminal-command\" class=\"flex flex-wrap items-baseline gap-x-1.5\">\n              <span class=\"text-success shrink-0 font-semibold\">{{ line.prompt }}</span>\n              <span>{{ line.command }}</span>\n            </div>\n          }\n          @if (line.output) {\n            <div data-slot=\"terminal-output\" class=\"text-muted-foreground\">{{ line.output }}</div>\n          }\n        </div>\n      }\n    </div>\n  `,\n})\nexport class UiTerminalComponent implements AfterViewInit, OnDestroy {\n  @ViewChild('body', { static: true }) bodyRef?: ElementRef<HTMLDivElement>\n\n  private readonly _lines = signal<TerminalLine[]>([])\n  private readonly _promptChar = signal('$')\n  readonly visibleCount = signal(0)\n  private timer: ReturnType<typeof setTimeout> | null = null\n  private raf = 0\n  private ready = false\n\n  /** Command history to render. */\n  @Input() set lines(v: TerminalLine[]) {\n    const prevLen = this._lines().length\n    this._lines.set(v ?? [])\n    if (!this.ready) {\n      this.visibleCount.set(this.typing ? 0 : this._lines().length)\n    } else if (this._lines().length !== prevLen) {\n      this.onLinesChanged()\n    }\n  }\n  get lines(): TerminalLine[] {\n    return this._lines()\n  }\n  /** Window title shown in the title bar. */\n  @Input() title = 'bash'\n  /** Prompt character. */\n  @Input() set promptChar(v: string) {\n    this._promptChar.set(v)\n  }\n  get promptChar(): string {\n    return this._promptChar()\n  }\n  @Input() theme: 'dark' | 'light' = 'dark'\n  /** Auto-scroll to bottom when new lines arrive. */\n  @Input({ transform: booleanAttribute }) autoScroll = true\n  /** Animate lines typing in one-by-one. */\n  @Input({ transform: booleanAttribute }) typing = false\n  /** Typing speed in ms per line. */\n  @Input() typingSpeed = 120\n  /** Max height before scrolling. */\n  @Input() maxHeight = '400px'\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn(\n      'block relative overflow-hidden rounded-lg border font-mono text-sm shadow-sm',\n      this.theme === 'dark' ? 'bg-card text-card-foreground border-border' : 'bg-muted border-border',\n      this.className,\n    )\n  }\n\n  resolvedLines(): ResolvedTerminalLine[] {\n    const promptChar = this._promptChar()\n    return this._lines().map((l) => ({\n      ...l,\n      type: l.type ?? (l.prompt || l.command ? 'command' : 'output'),\n      prompt: l.prompt ?? (l.type === 'output' ? '' : promptChar),\n    }))\n  }\n\n  shownLines(): ResolvedTerminalLine[] {\n    return this.resolvedLines().slice(0, this.visibleCount())\n  }\n\n  scrollToBottom(): void {\n    const body = this.bodyRef?.nativeElement\n    if (!this.autoScroll || !body) return\n    body.scrollTop = body.scrollHeight\n  }\n\n  ngAfterViewInit(): void {\n    this.ready = true\n    if (this.typing) {\n      this.visibleCount.set(0)\n      this.scheduleTyping()\n    } else {\n      this.visibleCount.set(this._lines().length)\n      this.raf = requestAnimationFrame(() => this.scrollToBottom())\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.clearTimer()\n    cancelAnimationFrame(this.raf)\n  }\n\n  private onLinesChanged(): void {\n    if (this.typing) {\n      this.clearTimer()\n      this.visibleCount.set(0)\n      this.scheduleTyping()\n      return\n    }\n    this.visibleCount.set(this._lines().length)\n    cancelAnimationFrame(this.raf)\n    this.raf = requestAnimationFrame(() => this.scrollToBottom())\n  }\n\n  private scheduleTyping(): void {\n    if (!this.typing || this.visibleCount() >= this._lines().length) return\n    this.timer = setTimeout(() => {\n      this.timer = null\n      this.visibleCount.update((c) => c + 1)\n      this.scrollToBottom()\n      this.scheduleTyping()\n    }, this.typingSpeed)\n  }\n\n  private clearTimer(): void {\n    if (this.timer) clearTimeout(this.timer)\n    this.timer = null\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/terminal/terminal.component.ts"
    },
    {
      "path": "packages/registry-angular/components/terminal/index.ts",
      "content": "export { UiTerminalComponent, type TerminalLine, type ResolvedTerminalLine } from './terminal.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/terminal/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Terminal/command-line display with a macOS-style title bar, command history, prompt character, dark/light themes, auto-scroll, optional typing animation, and a max-height scroll body.",
  "categories": [
    "display"
  ]
}