# Cursor Terminal

> VS Code / Cursor panel mock with PowerShell prompt and a typed shadcn add flow for the hero-section-v1 registry block.

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

- Page, with a live preview: https://shadcn.exchange/r/uiception/cursor-terminal
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiception/cursor-terminal-light-8b9425e31f126c08.png
- Registry item: https://uiception.com/r/cursor-terminal.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uiception.com/r/cursor-terminal.json
```

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

## Files

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

```tsx
import CursorTerminal from "./components/cursor-terminal"

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

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

```tsx
"use client"

import {
  ChevronDown,
  ChevronUp,
  MoreHorizontal,
  Plus,
  Terminal,
  X,
} 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 PanelIconButton from "./panel-icon-button"
import { ICON_SM, ICON_XS, PANEL_TABS, type LineColor } from "../lib/config"

import "../styles/cursor-terminal.css"

function colorCls(c?: LineColor) {
  if (c === "green") return "text-[#16c60c]"
  if (c === "cyan") return "text-[#61d6d6]"
  if (c === "dim") return "text-[#767676]"
  return "text-[#cccccc]"
}

function renderFirstStepCommand(text: string) {
  const npxToken = "npx"
  const commandPrefix = "npx shadcn@latest add "

  if (!text.startsWith(npxToken)) {
    return <span className="min-w-0">{text}</span>
  }

  const npxEnd = npxToken.length
  const argStart = commandPrefix.length

  return (
    <span className="min-w-0">
      <span className="text-[#dcdcaa]">{npxToken}</span>
      {text.slice(npxEnd, Math.min(text.length, argStart))}
      {text.length > argStart && (
        <span className="text-[#4fc1ff]">{text.slice(argStart)}</span>
      )}
    </span>
  )
}

export default function CursorTerminal({ className }: { className?: string }) {
  const { lines, prompt, scrollRef } = useTerminalAnimation()

  return (
    <div
      className={cn(
        "crt-terminal mx-auto w-full max-w-3xl min-w-0 overflow-hidden rounded-md border border-white/10 bg-[#1e1e1e] font-mono shadow-[0_20px_40px_-12px_rgb(0_0_0/0.4)]",
        className
      )}
    >
      <div className="flex h-9 min-h-9 items-stretch border-b border-white/6 bg-[#252526] select-none">
        <div className="flex min-w-0 flex-1 items-stretch overflow-x-auto [&::-webkit-scrollbar]:hidden">
          {PANEL_TABS.map((name) => {
            const active = name === "Terminal"
            return (
              <button
                key={name}
                type="button"
                aria-hidden
                tabIndex={-1}
                className={cn(
                  "shrink-0 border-t border-t-transparent px-3 text-[10px] transition-colors",
                  active
                    ? "border-t-[#007fd4] bg-[#1e1e1e] text-[#e8e8e8]"
                    : "text-[#858585] hover:text-[#c0c0c0]"
                )}
              >
                {name}
              </button>
            )
          })}
        </div>

        <div className="flex shrink-0 items-center gap-px pr-1 pl-2">
          <button
            type="button"
            aria-hidden
            tabIndex={-1}
            className="flex h-7 items-center gap-1 rounded px-1.5 text-[10px] text-[#cccccc] transition-colors hover:bg-white/6"
          >
            <Terminal className={cn(ICON_SM, "text-[#3b8eea]")} aria-hidden />
            <span className="max-w-22 truncate sm:max-w-none">powershell</span>
            <ChevronDown
              className={cn(ICON_XS, "text-[#858585]")}
              aria-hidden
            />
          </button>
          <PanelIconButton
            label="New terminal"
            className="w-auto min-w-7 gap-0.5 px-1"
          >
            <Plus className={ICON_SM} aria-hidden />
            <ChevronDown
              className={cn(ICON_XS, "text-[#a0a0a0]")}
              aria-hidden
            />
          </PanelIconButton>
          <PanelIconButton label="More">
            <MoreHorizontal className={ICON_SM} aria-hidden />
          </PanelIconButton>
          <PanelIconButton label="Maximize panel">
            <ChevronUp className={ICON_SM} aria-hidden />
          </PanelIconButton>
          <PanelIconButton label="Close panel" hoverDanger>
            <X className={ICON_SM} aria-hidden />
          </PanelIconButton>
        </div>
      </div>

      <div
        ref={scrollRef}
        className="crt-body h-88 overflow-y-auto bg-[#1e1e1e] px-3.5 py-2 text-left font-mono text-[11px] leading-[1.45] text-[#cccccc] [&::-webkit-scrollbar]:hidden"
        style={{ scrollbarWidth: "none" }}
      >
        {lines.map((line) =>
          line.promptLine ? (
            <div key={line.id}>
              <div className="flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0">
                <PromptShell dot={line.dot ?? "grey"} showPath />
                {renderFirstStepCommand(line.text)}
              </div>
            </div>
          ) : (
            <div
              key={line.id}
              className={cn("crt-line-in", colorCls(line.color))}
            >
              {line.spinning ? (
                <span>
                  <Spinner /> {line.text}
                </span>
              ) : (
                line.text || "\u00A0"
              )}
            </div>
          )
        )}

        {prompt.showPrompt && (
          <div
            className={cn(
              "flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0",
              prompt.animateIn && "crt-line-in"
            )}
          >
            <PromptShell dot={prompt.dot} showPath={prompt.showPath} />
            {prompt.typed && (
              <span className={cn("min-w-0", prompt.pasteIn && "crt-paste-in")}>
                {renderFirstStepCommand(prompt.typed)}
              </span>
            )}
            {prompt.cursor && (
              <span
                className="crt-cursor ml-px inline-block h-[1em] w-2 translate-y-px bg-[#cccccc] align-text-bottom"
                aria-hidden
              />
            )}
          </div>
        )}
      </div>

      <div className="border-t border-white/6 bg-[#1e1e1e] py-1.5 text-center text-[10px] text-[#6e6e6e]">
        Ctrl+K to generate command
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/cursor-terminal/components/panel-icon-button.tsx`

```tsx
import type { ReactNode } from "react"
import { cn } from "@/lib/utils"

export default function PanelIconButton({
  children,
  label,
  hoverDanger,
  className,
  onClick,
  ariaPressed,
  tabIndex = -1,
}: {
  children: ReactNode
  label: string
  hoverDanger?: boolean
  className?: string
  onClick?: () => void
  ariaPressed?: boolean | "mixed"
  tabIndex?: number
}) {
  return (
    <button
      type="button"
      aria-label={label}
      title={label}
      aria-pressed={ariaPressed}
      tabIndex={tabIndex}
      onClick={onClick}
      className={cn(
        "flex size-7 items-center justify-center rounded text-[#c8c8c8] transition-colors hover:bg-white/6",
        hoverDanger && "hover:bg-[rgb(196_43_28/0.9)] hover:text-white",
        className
      )}
    >
      {children}
    </button>
  )
}
```

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

```tsx
import { PROMPT } from "../lib/config"
import type { PromptState } from "../lib/config"
import { cn } from "@/lib/utils"

type Props = Pick<PromptState, "dot" | "showPath">

export default function PromptShell({ dot, showPath }: Props) {
  return (
    <div className="flex items-center gap-2">
      <span
        className={cn(
          "inline-block size-[7px] shrink-0 rounded-full ring-1 ring-white/15",
          dot === "blue" ? "bg-[#3b8eea]" : "bg-[#767676]"
        )}
        aria-hidden
      />
      {showPath && <span className="tabular-nums">{PROMPT}</span>}
    </div>
  )
}
```

### `registry/new-york/blocks/cursor-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/cursor-terminal/hooks/use-terminal-animation.ts`

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

import {
  COMMAND, INIT_LINES, OUTPUT_SEQUENCE, PASTE_SETTLE_MS,
  type Line, type Phase, type PromptState,
} from "../lib/config"

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 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])

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

  useEffect(() => {
    if (phase !== "typing") return
    const t = setTimeout(() => {
      setLines((prev) => [...prev, { id: idRef.current++, text: COMMAND, promptLine: true, dot: "grey" }])
      setTyped("")
      setOutIdx(0)
      setPhase("output")
    }, PASTE_SETTLE_MS)
    return () => clearTimeout(t)
  }, [phase])

  useEffect(() => {
    if (phase !== "output") return
    if (outIdx >= OUTPUT_SEQUENCE.length) {
      const t = setTimeout(() => setPhase("done"), 500)
      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 ?? item.text}`, 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 !== "done") return
    const blueTimer = setTimeout(() => {
      setLines((prev) => prev.map((line) => (line.promptLine ? { ...line, dot: "blue" } : line)))
    }, 700)
    return () => clearTimeout(blueTimer)
  }, [phase])

  const prompt = useMemo<PromptState>(() => {
    switch (phase) {
      case "idle":
        return { dot: "grey", showPrompt: true, showPath: true, typed: "", cursor: true, animateIn: false, pasteIn: false }
      case "typing":
        return { dot: "grey", showPrompt: true, showPath: true, typed, cursor: true, animateIn: false, pasteIn: true }
      case "output":
        return { dot: "grey", showPrompt: false, showPath: false, typed: "", cursor: false, animateIn: false, pasteIn: false }
      case "done":
        return {
          dot: "grey",
          showPrompt: true,
          showPath: true,
          typed: "",
          cursor: false,
          animateIn: true,
          pasteIn: false,
        }
    }
  }, [phase, typed])

  return { lines, prompt, scrollRef }
}
```

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

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

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

export interface PromptState {
  dot: "blue" | "grey"
  showPrompt: boolean
  showPath: boolean
  typed: string
  cursor: boolean
  animateIn: boolean
  pasteIn: boolean
}

export interface Line {
  id: number
  text: string
  color?: LineColor
  spinning?: boolean
  promptLine?: boolean
  dot?: "blue" | "grey"
}

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

export const PROMPT = "PS C:\\projects\\my-app> "
export const COMMAND = 'npx shadcn@latest add "https://uiception.com/r/hero-section-v1.json"'
/** Hold after paste before the command line commits. */
export const PASTE_SETTLE_MS = 580

export const ICON_SM = "size-3 shrink-0 stroke-[1.75]"
export const ICON_XS = "size-2.5 shrink-0 stroke-[1.75]"

export const PANEL_TABS = ["Problems", "Output", "Debug Console", "Terminal", "Ports", "GitLens"] as const

export const INIT_LINES: Line[] = []

export const OUTPUT_SEQUENCE: OutputItem[] = [
  { type: "spinner", text: "Checking registry.", delay: 420, duration: 1500 },
  {
    type: "spinner",
    text: "Installing dependencies.",
    delay: 240,
    duration: 3000,
    doneText: "Installed dependencies.",
  },
  { type: "line", text: "✓ Created 33 files:", color: "green", delay: 320 },
  { type: "line", text: "  - components/ui/button.tsx", color: "cyan", delay: 155 },
  { type: "line", text: "  - app/hero-section-v1/page.tsx", color: "cyan", delay: 145 },
  { type: "line", text: "  - app/hero-section-v1/components/hero-section-v1.tsx", color: "cyan", delay: 150 },
  { type: "line", text: "  - app/brands-section-v1/components/brands-section-v1.tsx", color: "cyan", delay: 142 },
  { type: "line", text: "  - app/mac-os-terminal/components/mac-os-terminal.tsx", color: "cyan", delay: 148 },
  { type: "line", text: "  - public/images/blocks/hero-section-v1/image.png.gitkeep", color: "cyan", delay: 152 },
  {
    type: "line",
    text: "  … +27 files (block-media, terminal TS/CSS, code segments, 11 brand SVGs)",
    color: "dim",
    delay: 190,
  },
  { type: "line", text: "", delay: 260 },
  { type: "line", text: "Success! Component installed successfully.", color: "green", delay: 300 },
  { type: "line", text: "You may now import and use the component.", color: "dim", delay: 180 },
]
```

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

```css
/* cursor-terminal — caret blink, line fade-in, paste-in */

.crt-cursor {
  animation: crt-terminal-blink 1.05s step-end infinite;
  margin-left: 1px;
  display: inline-block;
  height: 1em;
  width: 0.5rem;
  translate: 0 1px;
  vertical-align: text-bottom;
  background-color: var(--crt-t-cursor-bg, #cccccc);
}

.crt-line-in {
  animation: crt-terminal-line-fade 0.07s linear both;
}

.crt-paste-in {
  animation: crt-terminal-paste-in 0.42s cubic-bezier(0.2, 1, 0.32, 1) both;
}

@keyframes crt-terminal-blink {
  0%,
  48% {
    opacity: 1;
  }
  52%,
  100% {
    opacity: 0;
  }
}

@keyframes crt-terminal-line-fade {
  from {
    opacity: 0.35;
  }
  to {
    opacity: 1;
  }
}

@keyframes crt-terminal-paste-in {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0% 0 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .crt-cursor {
    animation: none;
    opacity: 1;
  }

  .crt-line-in,
  .crt-paste-in {
    animation: none;
  }
}
```
