{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal",
  "type": "registry:ui",
  "title": "Terminal",
  "description": "A window that types its commands out and prints their output a beat later.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/terminal.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface LineState {\n  /** This line is the one currently running. */\n  active: boolean\n  /** Milliseconds per character, inherited from the terminal. */\n  speed: number\n  /** Play everything at once with no typing. */\n  instant: boolean\n  /** Hand the session on to the next line. */\n  done: () => void\n}\n\nconst LineContext = React.createContext<LineState>({\n  active: false,\n  speed: 34,\n  instant: true,\n  done: () => {},\n})\n\nexport interface TerminalProps extends React.ComponentProps<\"div\"> {\n  /** Text in the window bar. A path or a shell name, usually. */\n  title?: string\n  /** Milliseconds a single character takes to be typed. */\n  speed?: number\n  /** Hold the session until the window scrolls into view. */\n  startOnView?: boolean\n  /** Print the whole session at once with no typing. */\n  instant?: boolean\n}\n\n/**\n * Lines are rendered as the session reaches them rather than hidden and\n * revealed, so a line that has not run yet is not in the document at all and\n * cannot be read out ahead of itself.\n */\nexport function Terminal({\n  children,\n  className,\n  title = \"bash\",\n  speed = 34,\n  startOnView = true,\n  instant = false,\n  ...props\n}: TerminalProps) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const lines = React.Children.toArray(children)\n  const [started, setStarted] = React.useState(!startOnView)\n  const [step, setStep] = React.useState(0)\n  const [reduced, setReduced] = React.useState(false)\n\n  React.useEffect(() => {\n    setReduced(window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches)\n  }, [])\n\n  React.useEffect(() => {\n    if (!startOnView) {\n      return\n    }\n\n    const node = ref.current\n    if (!node || typeof IntersectionObserver === \"undefined\") {\n      setStarted(true)\n      return\n    }\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          setStarted(true)\n          observer.disconnect()\n        }\n      },\n      { threshold: 0.3 }\n    )\n\n    observer.observe(node)\n    return () => observer.disconnect()\n  }, [startOnView])\n\n  const atOnce = instant || reduced\n  const reached = atOnce ? lines.length - 1 : step\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"terminal\"\n      className={cn(\n        \"border-border bg-card w-full overflow-hidden rounded-xl border font-mono text-sm\",\n        className\n      )}\n      {...props}\n    >\n      <div className=\"border-border bg-muted/50 flex items-center gap-2 border-b px-4 py-2.5\">\n        <span aria-hidden=\"true\" className=\"flex gap-1.5\">\n          <span className=\"bg-muted-foreground/30 size-2.5 rounded-full\" />\n          <span className=\"bg-muted-foreground/30 size-2.5 rounded-full\" />\n          <span className=\"bg-muted-foreground/30 size-2.5 rounded-full\" />\n        </span>\n        <span className=\"text-muted-foreground text-xs\">{title}</span>\n      </div>\n\n      <div className=\"flex flex-col gap-1 p-4 leading-relaxed\">\n        {lines.map((line, index) => {\n          if (started && index > reached) {\n            return null\n          }\n\n          return (\n            <LineContext.Provider\n              key={index}\n              value={{\n                active: started && index === reached && !atOnce,\n                speed,\n                instant: atOnce || index < step,\n                done: () =>\n                  setStep((current) =>\n                    current === index ? current + 1 : current\n                  ),\n              }}\n            >\n              {started ? line : null}\n            </LineContext.Provider>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nexport interface TerminalCommandProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\"\n> {\n  /** The command, as plain text so the typing stays predictable. */\n  children: string\n  /** What sits in front of the command. */\n  prompt?: string\n}\n\nexport function TerminalCommand({\n  children,\n  className,\n  prompt = \"$\",\n  ...props\n}: TerminalCommandProps) {\n  const { active, speed, instant, done } = React.useContext(LineContext)\n  const [typed, setTyped] = React.useState(instant ? children.length : 0)\n\n  React.useEffect(() => {\n    if (instant) {\n      setTyped(children.length)\n      return\n    }\n\n    if (!active) {\n      return\n    }\n\n    if (typed >= children.length) {\n      done()\n      return\n    }\n\n    const timer = window.setTimeout(() => setTyped(typed + 1), speed)\n    return () => window.clearTimeout(timer)\n  }, [active, instant, typed, children.length, speed, done])\n\n  return (\n    <div\n      data-slot=\"terminal-command\"\n      className={cn(\"flex gap-2\", className)}\n      {...props}\n    >\n      <span aria-hidden=\"true\" className=\"text-muted-foreground select-none\">\n        {prompt}\n      </span>\n      <span className=\"break-all\">\n        {children.slice(0, typed)}\n        {active && typed < children.length ? (\n          <span\n            aria-hidden=\"true\"\n            className=\"animate-caret-blink bg-foreground ml-0.5 inline-block h-[1em] w-[0.5ch] translate-y-[0.15em] motion-reduce:animate-none\"\n          />\n        ) : null}\n      </span>\n    </div>\n  )\n}\n\nexport interface TerminalOutputProps extends React.ComponentProps<\"div\"> {\n  /** Milliseconds the command appears to run before this prints. */\n  delay?: number\n}\n\nexport function TerminalOutput({\n  children,\n  className,\n  delay = 260,\n  ...props\n}: TerminalOutputProps) {\n  const { active, instant, done } = React.useContext(LineContext)\n  const [shown, setShown] = React.useState(instant)\n\n  React.useEffect(() => {\n    if (instant) {\n      setShown(true)\n      return\n    }\n\n    if (!active || shown) {\n      if (shown) {\n        done()\n      }\n      return\n    }\n\n    const timer = window.setTimeout(() => setShown(true), delay)\n    return () => window.clearTimeout(timer)\n  }, [active, instant, shown, delay, done])\n\n  if (!shown) {\n    return null\n  }\n\n  return (\n    <div\n      data-slot=\"terminal-output\"\n      className={cn(\n        \"text-muted-foreground animate-terminal-print break-all\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-out-quart": "cubic-bezier(0.165, 0.84, 0.44, 1)",
      "animate-caret-blink": "caret-blink 1.1s steps(2, jump-none) infinite",
      "animate-terminal-print": "terminal-print 240ms var(--ease-out-quart) both"
    }
  },
  "css": {
    "@keyframes caret-blink": {
      "0%, 100%": {
        "opacity": "1"
      },
      "50%": {
        "opacity": "0"
      }
    },
    "@keyframes terminal-print": {
      "from": {
        "opacity": "0",
        "transform": "translateY(3px)"
      },
      "to": {
        "opacity": "1",
        "transform": "translateY(0)"
      }
    }
  }
}