# Mac OS Terminal

> macOS Terminal window mock with zsh prompt, typed CLI demo, and shadcn install outputâ€”ideal for integrations and developer tooling sections.

A shadcn/ui block from the [@uiception](https://uiception.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiception/mac-os-terminal).

- Page, with a live preview: https://shadcn.exchange/r/uiception/mac-os-terminal
- Screenshot: https://shadcn.exchange/shot/uiception/mac-os-terminal/light.png
- Registry item: https://uiception.com/r/mac-os-terminal.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uiception/mac-os-terminal.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `geist`

## Files

### `registry/new-york/blocks/mac-os-terminal/page.tsx`

```tsx
import MacOsTerminal from "./components/mac-os-terminal"

export default function Page() {
  return (
    <div className="px-4 py-10 md:px-6 md:py-14">
      <MacOsTerminal />
    </div>
  )
}
```

### `registry/new-york/blocks/mac-os-terminal/components/mac-os-terminal.tsx`

```tsx
"use client"

import { GeistSans } from "geist/font/sans"
import { Folder } from "lucide-react"
import { cn } from "@/lib/utils"

import { useTerminalAnimation } from "../hooks/use-terminal-animation"
import Spinner from "./spinner"
import PromptShell from "./prompt-shell"
import TrafficLights from "./traffic-lights"
import SyntaxCode from "./syntax-code"
import { CODE_DEMO } from "../lib/mac-os-terminal-code-segments"
import type { LineColor } from "../lib/config"

import "../styles/mac-os-terminal.css"

const codeBlockWrap =
  "mot-codeblock my-2 border-l-2 py-2 pr-2 pl-3 font-mono text-[11px] leading-[1.45]"

function colorCls(c?: LineColor) {
  if (!c) return "text-[var(--mot-t-body-fg)]"
  switch (c) {
    case "green":
      return "text-[var(--mot-t-line-green)]"
    case "cyan":
      return "text-[var(--mot-t-line-cyan)]"
    case "dim":
      return "text-[var(--mot-t-line-dim)]"
  }
}

export default function MacOsTerminal({ className }: { className?: string }) {
  const { lines, typed, phase, codeIdx, scrollRef } = useTerminalAnimation()

  return (
    <div
      className={cn(
        "mot-terminal mx-auto w-full max-w-3xl min-w-0 overflow-hidden rounded-md border border-(--mot-t-border) bg-(--mot-t-shell-bg) font-mono shadow-(--mot-t-shadow) outline-none outline-1 -outline-offset-1 outline-(--mot-t-ring)",
        className
      )}
    >
      <div className="relative flex h-7 shrink-0 items-center border-b border-(--mot-t-titlebar-border) bg-(--mot-t-titlebar-bg) select-none">
        <TrafficLights />
        <div
          className={cn(
            GeistSans.className,
            "flex w-full items-center justify-center gap-1.5 px-20 text-center text-[10px] font-medium tracking-tight text-(--mot-t-title-fg)"
          )}
        >
          <Folder
            size={12}
            className="size-3 shrink-0"
            fill="#3b8eea"
            stroke="none"
            strokeWidth={0}
            aria-hidden
          />
          <span>Projects</span>
          <span className="text-[#a1a1aa] dark:text-[#6b6b6b]" aria-hidden>
            —
          </span>
          <span>-zsh</span>
          <span className="text-[#a1a1aa] dark:text-[#6b6b6b]" aria-hidden>
            —
          </span>
          <span className="tabular-nums">80×24</span>
        </div>
      </div>

      <div
        ref={scrollRef}
        className="mot-terminal-body h-128 overflow-y-auto bg-(--mot-t-body-bg) px-3.5 py-2 text-left font-mono text-[11px] leading-[1.45] text-(--mot-t-body-fg) [&::-webkit-scrollbar]:hidden"
        style={{ scrollbarWidth: "none" }}
      >
        {lines.map((line) => (
          <div
            key={line.id}
            className={cn("mot-terminal-line-in", colorCls(line.color))}
          >
            {line.spinning ? (
              <span>
                <Spinner /> {line.text}
              </span>
            ) : (
              line.text || "\u00A0"
            )}
          </div>
        ))}

        {(phase === "typingCode" || phase === "done") && (
          <div className={codeBlockWrap}>
            <pre className="m-0 font-mono wrap-break-word whitespace-pre-wrap">
              <SyntaxCode
                visibleChars={phase === "done" ? CODE_DEMO.length : codeIdx}
              />
              {phase === "typingCode" && (
                <span className="mot-terminal-cursor ml-px inline-block h-[1em] w-2 translate-y-px bg-(--mot-terminal-cursor-bg) align-text-bottom" />
              )}
            </pre>
          </div>
        )}

        {(phase === "idle" || phase === "typing") && (
          <div className="flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0">
            <PromptShell />
            <span className="min-w-0">{typed}</span>
            <span className="mot-terminal-cursor ml-px inline-block h-[1em] w-2 translate-y-px bg-(--mot-terminal-cursor-bg) align-text-bottom" />
          </div>
        )}

        {phase === "done" && (
          <div className="mt-2 flex items-center gap-x-1">
            <PromptShell />
            <span className="mot-terminal-cursor ml-px inline-block h-[1em] w-2 translate-y-px bg-(--mot-terminal-cursor-bg) align-text-bottom" />
          </div>
        )}
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/mac-os-terminal/components/prompt-shell.tsx`

```tsx
import type { ReactNode } from "react"
import { PROMPT } from "../lib/config"

export default function PromptShell({ children }: { children?: ReactNode }) {
  return (
    <div className="flex min-w-0 flex-wrap items-center gap-x-1">
      <span className="shrink-0 tabular-nums">{PROMPT}</span>
      {children}
    </div>
  )
}
```

### `registry/new-york/blocks/mac-os-terminal/components/spinner.tsx`

```tsx
"use client"

import { useEffect, useState } from "react"

const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]

export default function Spinner() {
  const [frame, setFrame] = useState(0)
  useEffect(() => {
    const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80)
    return () => clearInterval(t)
  }, [])
  return <span>{FRAMES[frame]}</span>
}
```

### `registry/new-york/blocks/mac-os-terminal/components/syntax-code.tsx`

```tsx
import type { ReactNode } from "react"
import { CODE_SYNTAX_PARTS } from "../lib/mac-os-terminal-code-segments"

export default function SyntaxCode({ visibleChars }: { visibleChars: number }) {
  let remaining = visibleChars
  const nodes: ReactNode[] = []
  let key = 0
  for (const { t, c } of CODE_SYNTAX_PARTS) {
    if (remaining <= 0) break
    const take = Math.min(t.length, remaining)
    if (take > 0) {
      nodes.push(
        <span key={key++} className={c}>
          {t.slice(0, take)}
        </span>
      )
      remaining -= take
    }
  }
  return <>{nodes}</>
}
```

### `registry/new-york/blocks/mac-os-terminal/components/traffic-lights.tsx`

```tsx
export default function TrafficLights() {
  return (
    <div className="absolute left-3 flex gap-2" aria-hidden>
      <button
        type="button"
        tabIndex={-1}
        className="size-3 shrink-0 rounded-full bg-[#ff5f57] ring-1 ring-black/10"
      />
      <button
        type="button"
        tabIndex={-1}
        className="size-3 shrink-0 rounded-full bg-[#febc2e] ring-1 ring-black/10"
      />
      <button
        type="button"
        tabIndex={-1}
        className="size-3 shrink-0 rounded-full bg-[#28c840] ring-1 ring-black/10"
      />
    </div>
  )
}
```

### `registry/new-york/blocks/mac-os-terminal/hooks/use-terminal-animation.ts`

```ts
// Phase flow: idle → typing → output → typingCode → done → idle (loops)
import { useEffect, useRef, useState } from "react"

import {
  COMMAND, INIT_LINES, OUTPUT_SEQUENCE, PROMPT,
  TYPE_CODE_MS, TYPE_CODE_NEWLINE_MS, TYPE_SPEED,
  type Line, type Phase,
} from "../lib/config"
import { CODE_DEMO } from "../lib/mac-os-terminal-code-segments"

export function useTerminalAnimation() {
  const [lines, setLines] = useState<Line[]>(INIT_LINES)
  const [typed, setTyped] = useState("")
  const [phase, setPhase] = useState<Phase>("idle")
  const [outIdx, setOutIdx] = useState(0)
  const [codeIdx, setCodeIdx] = useState(0)
  const idRef = useRef(10)
  const spinTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const scrollRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight
    }
  }, [lines, typed, codeIdx, phase])

  useEffect(() => {
    if (phase !== "idle") return
    const t = setTimeout(() => setPhase("typing"), 1600)
    return () => clearTimeout(t)
  }, [phase])

  useEffect(() => {
    if (phase !== "typing") return
    if (typed.length < COMMAND.length) {
      const t = setTimeout(() => setTyped(COMMAND.slice(0, typed.length + 1)), TYPE_SPEED)
      return () => clearTimeout(t)
    }
    const t = setTimeout(() => {
      setLines((prev) => [...prev, { id: idRef.current++, text: `${PROMPT}${COMMAND}` }])
      setTyped("")
      setOutIdx(0)
      setPhase("output")
    }, 360)
    return () => clearTimeout(t)
  }, [phase, typed])

  useEffect(() => {
    if (phase !== "output") return
    if (outIdx >= OUTPUT_SEQUENCE.length) {
      const t = setTimeout(() => { setCodeIdx(0); setPhase("typingCode") }, 450)
      return () => clearTimeout(t)
    }
    const item = OUTPUT_SEQUENCE[outIdx]
    if (item.type === "spinner") {
      const t = setTimeout(() => {
        const spinId = idRef.current++
        setLines((prev) => [...prev, { id: spinId, text: item.text, spinning: true }])
        spinTimerRef.current = setTimeout(() => {
          setLines((prev) =>
            prev.map((l) =>
              l.id === spinId
                ? { ...l, spinning: false, text: `✓ ${item.doneText}`, color: "green" as const }
                : l,
            ),
          )
          setOutIdx((i) => i + 1)
        }, item.duration)
      }, item.delay)
      return () => {
        clearTimeout(t)
        if (spinTimerRef.current) clearTimeout(spinTimerRef.current)
      }
    }
    const t = setTimeout(() => {
      setLines((prev) => [...prev, { id: idRef.current++, text: item.text, color: item.color }])
      setOutIdx((i) => i + 1)
    }, item.delay)
    return () => clearTimeout(t)
  }, [phase, outIdx])

  useEffect(() => {
    if (phase !== "typingCode") return
    if (codeIdx >= CODE_DEMO.length) {
      const t = setTimeout(() => setPhase("done"), 320)
      return () => clearTimeout(t)
    }
    const ch = CODE_DEMO[codeIdx]
    const delay = ch === "\n" ? TYPE_CODE_NEWLINE_MS : TYPE_CODE_MS
    const t = setTimeout(() => setCodeIdx((i) => i + 1), delay)
    return () => clearTimeout(t)
  }, [phase, codeIdx])

  useEffect(() => {
    if (phase !== "done") return
    const t = setTimeout(() => {
      idRef.current = 10
      setTyped("")
      setOutIdx(0)
      setCodeIdx(0)
      setLines(INIT_LINES)
      setPhase("idle")
    }, 8000)
    return () => clearTimeout(t)
  }, [phase])

  return { lines, typed, phase, codeIdx, scrollRef }
}
```

### `registry/new-york/blocks/mac-os-terminal/lib/config.ts`

```ts
export type LineColor = "green" | "cyan" | "dim"

export type Phase = "idle" | "typing" | "output" | "typingCode" | "done"

export interface Line {
  id: number
  text: string
  color?: LineColor
  spinning?: boolean
}

export type OutputItem =
  | { type: "line"; text: string; color?: LineColor; delay: number }
  | { type: "spinner"; text: string; doneText: string; delay: number; duration: number }

export const PROMPT = "user@macbook app % "
export const COMMAND = "pnpm i ai"
export const TYPE_SPEED = 38
export const TYPE_CODE_MS = 11
export const TYPE_CODE_NEWLINE_MS = 48

export const INIT_LINES: Line[] = []

export const OUTPUT_SEQUENCE: OutputItem[] = [
  {
    type: "spinner",
    text: "Resolving dependencies…",
    doneText: "Resolved dependencies",
    delay: 240,
    duration: 900,
  },
  {
    type: "spinner",
    text: "Downloading + installing ai…",
    doneText: "Downloaded and installed ai",
    delay: 140,
    duration: 1400,
  },
]
```

### `registry/new-york/blocks/mac-os-terminal/lib/mac-os-terminal-code-segments.ts`

```ts
/** Token spans for AI SDK demo — joined string must match CODE_DEMO in mac-os-terminal.tsx */

export type MotShClass =
  | "mot-sh-kw"
  | "mot-sh-str"
  | "mot-sh-fn"
  | "mot-sh-num"
  | "mot-sh-prop"
  | "mot-sh-var"
  | "mot-sh-punct"

export const CODE_SYNTAX_PARTS: ReadonlyArray<{ t: string; c: MotShClass }> = [
  { t: "import ", c: "mot-sh-kw" },
  { t: "{ ", c: "mot-sh-punct" },
  { t: "generateText", c: "mot-sh-fn" },
  { t: ", ", c: "mot-sh-punct" },
  { t: "tool", c: "mot-sh-fn" },
  { t: " } ", c: "mot-sh-punct" },
  { t: "from ", c: "mot-sh-kw" },
  { t: "'ai'", c: "mot-sh-str" },
  { t: ";\n", c: "mot-sh-punct" },
  { t: "import ", c: "mot-sh-kw" },
  { t: "{ ", c: "mot-sh-punct" },
  { t: "z", c: "mot-sh-fn" },
  { t: " } ", c: "mot-sh-punct" },
  { t: "from ", c: "mot-sh-kw" },
  { t: "'zod'", c: "mot-sh-str" },
  { t: ";\n\n", c: "mot-sh-punct" },
  { t: "const", c: "mot-sh-kw" },
  { t: " { ", c: "mot-sh-punct" },
  { t: "text", c: "mot-sh-var" },
  { t: " } ", c: "mot-sh-punct" },
  { t: "= ", c: "mot-sh-punct" },
  { t: "await", c: "mot-sh-kw" },
  { t: " ", c: "mot-sh-punct" },
  { t: "generateText", c: "mot-sh-fn" },
  { t: "({\n  ", c: "mot-sh-punct" },
  { t: "model", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "'openai/gpt-5.2'", c: "mot-sh-str" },
  { t: ",\n  ", c: "mot-sh-punct" },
  { t: "prompt", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "'What is the weather like today in San Francisco?'", c: "mot-sh-str" },
  { t: ",\n  ", c: "mot-sh-punct" },
  { t: "tools", c: "mot-sh-prop" },
  { t: ": {\n    ", c: "mot-sh-punct" },
  { t: "getWeather", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "tool", c: "mot-sh-fn" },
  { t: "({\n      ", c: "mot-sh-punct" },
  { t: "description", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "'Get the weather in a location'", c: "mot-sh-str" },
  { t: ",\n      ", c: "mot-sh-punct" },
  { t: "inputSchema", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "z", c: "mot-sh-fn" },
  { t: ".", c: "mot-sh-punct" },
  { t: "object", c: "mot-sh-fn" },
  { t: "({\n        ", c: "mot-sh-punct" },
  { t: "location", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "z", c: "mot-sh-fn" },
  { t: ".", c: "mot-sh-punct" },
  { t: "string", c: "mot-sh-fn" },
  { t: "()", c: "mot-sh-punct" },
  { t: ".", c: "mot-sh-punct" },
  { t: "describe", c: "mot-sh-fn" },
  { t: "(", c: "mot-sh-punct" },
  { t: "'The location to get the weather for'", c: "mot-sh-str" },
  { t: ")", c: "mot-sh-punct" },
  { t: ",\n      }),\n      ", c: "mot-sh-punct" },
  { t: "execute", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "async", c: "mot-sh-kw" },
  { t: " ", c: "mot-sh-punct" },
  { t: "({ ", c: "mot-sh-punct" },
  { t: "location", c: "mot-sh-var" },
  { t: " }) => ({\n        ", c: "mot-sh-punct" },
  { t: "location", c: "mot-sh-prop" },
  { t: ",\n        ", c: "mot-sh-punct" },
  { t: "temperature", c: "mot-sh-prop" },
  { t: ": ", c: "mot-sh-punct" },
  { t: "72", c: "mot-sh-num" },
  { t: " + ", c: "mot-sh-punct" },
  { t: "Math", c: "mot-sh-fn" },
  { t: ".", c: "mot-sh-punct" },
  { t: "floor", c: "mot-sh-fn" },
  { t: "(", c: "mot-sh-punct" },
  { t: "Math", c: "mot-sh-fn" },
  { t: ".", c: "mot-sh-punct" },
  { t: "random", c: "mot-sh-fn" },
  { t: "() * ", c: "mot-sh-punct" },
  { t: "21", c: "mot-sh-num" },
  { t: ") - ", c: "mot-sh-punct" },
  { t: "10", c: "mot-sh-num" },
  { t: ",\n      }),\n    }),\n  },\n});", c: "mot-sh-punct" },
]

export const CODE_DEMO = CODE_SYNTAX_PARTS.map((p) => p.t).join("")
```

### `registry/new-york/blocks/mac-os-terminal/styles/mac-os-terminal.css`

```css
/* mac-os-terminal — theme tokens, caret blink, line fade, syntax hooks */

.mot-terminal {
  --mot-t-shell-bg: #fafafa;
  --mot-t-body-bg: #ffffff;
  --mot-t-body-fg: #3f3f46;
  --mot-t-border: rgb(0 0 0 / 0.1);
  --mot-t-ring: rgb(0 0 0 / 0.06);
  --mot-t-shadow: 0 20px 40px -12px rgb(0 0 0 / 0.12);
  --mot-t-titlebar-bg: #ececec;
  --mot-t-titlebar-border: rgb(0 0 0 / 0.08);
  --mot-t-title-fg: #3c3c3c;
  --mot-t-line-green: #15803d;
  --mot-t-line-cyan: #0f766e;
  --mot-t-line-dim: #71717a;
  --mot-terminal-cursor-bg: #27272a;
}

.dark .mot-terminal {
  --mot-t-shell-bg: #1e1e1e;
  --mot-t-body-bg: #1e1e1e;
  --mot-t-body-fg: #cccccc;
  --mot-t-border: rgb(255 255 255 / 0.1);
  --mot-t-ring: rgb(0 0 0 / 0.2);
  --mot-t-shadow: 0 20px 40px -12px rgb(0 0 0 / 0.45);
  --mot-t-titlebar-bg: #2d2d2d;
  --mot-t-titlebar-border: rgb(255 255 255 / 0.08);
  --mot-t-title-fg: #d4d4d8;
  --mot-t-line-green: #16c60c;
  --mot-t-line-cyan: #61d6d6;
  --mot-t-line-dim: #767676;
  --mot-terminal-cursor-bg: #cccccc;
}

.mot-terminal-cursor {
  animation: mot-terminal-blink 1.15s step-start infinite;
}

.mot-terminal-line-in {
  animation: mot-terminal-line-fade 0.12s ease-out both;
}

@keyframes mot-terminal-blink {
  0%,
  49.9% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

@keyframes mot-terminal-line-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.mot-codeblock {
  --mot-sh-kw: #b83280;
  --mot-sh-str: #166534;
  --mot-sh-fn: #6d28d9;
  --mot-sh-num: #0369a1;
  --mot-sh-prop: #18181b;
  --mot-sh-var: #0e7490;
  --mot-sh-punct: #a21caf;
  --mot-cursor-fg: #27272a;

  border-left-width: 2px;
  border-left-style: solid;
  border-left-color: rgb(59 142 234 / 0.55);
  background-color: rgb(244 244 245 / 0.95);
}

.dark .mot-codeblock {
  --mot-sh-kw: #ff79c6;
  --mot-sh-str: #50fa7b;
  --mot-sh-fn: #bd93f9;
  --mot-sh-num: #8be9fd;
  --mot-sh-prop: #f8f8f2;
  --mot-sh-var: #8be9fd;
  --mot-sh-punct: #ff79c6;
  --mot-cursor-fg: #cccccc;

  border-left-color: rgb(59 142 234 / 0.45);
  background-color: rgb(0 0 0 / 0.55);
}

.mot-sh-kw {
  color: var(--mot-sh-kw);
}
.mot-sh-str {
  color: var(--mot-sh-str);
}
.mot-sh-fn {
  color: var(--mot-sh-fn);
}
.mot-sh-num {
  color: var(--mot-sh-num);
}
.mot-sh-prop {
  color: var(--mot-sh-prop);
}
.mot-sh-var {
  color: var(--mot-sh-var);
}
.mot-sh-punct {
  color: var(--mot-sh-punct);
}

.mot-codeblock .mot-terminal-cursor {
  background-color: var(--mot-cursor-fg);
}
```
