{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal",
  "title": "Terminal",
  "description": "A command display with copy support, multiline content, and keyboard-accessible command variants.",
  "dependencies": [
    "@base-ui/react",
    "lucide-react",
    "prism-react-renderer"
  ],
  "files": [
    {
      "path": "registry/rivetui/components/terminal/terminal.tsx",
      "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { Check, Copy } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { SyntaxHighlight } from \"@/lib/syntax-highlight\";\nimport { cn } from \"@/lib/utils\";\n\ntype TerminalCommand<TValue extends string = string> = {\n  label: string;\n  value: TValue;\n  command: string;\n  language?: string;\n};\n\ntype TerminalCommandList = readonly [TerminalCommand, ...TerminalCommand[]];\n\ntype StringWithAutocomplete<TValue extends string> = TValue | (string & Record<never, never>);\n\ntype TerminalCommonProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\"> & {\n  label?: React.ReactNode;\n  prompt?: React.ReactNode | false;\n  copyable?: boolean;\n  language?: string;\n};\n\ntype TerminalSingleProps = TerminalCommonProps & {\n  command: string;\n  commands?: never;\n  defaultValue?: never;\n  value?: never;\n  onValueChange?: never;\n};\n\ntype TerminalVariantsProps<TCommands extends TerminalCommandList = TerminalCommandList> =\n  TerminalCommonProps & {\n    command?: never;\n    commands: TCommands;\n    defaultValue?: StringWithAutocomplete<NoInfer<TCommands[number][\"value\"]>>;\n    value?: StringWithAutocomplete<NoInfer<TCommands[number][\"value\"]>>;\n    onValueChange?: (value: string) => void;\n  };\n\ntype TerminalProps<TCommands extends TerminalCommandList = TerminalCommandList> =\n  | TerminalSingleProps\n  | TerminalVariantsProps<TCommands>;\n\nfunction Terminal<const TCommands extends TerminalCommandList>(\n  props: TerminalVariantsProps<TCommands>,\n): React.JSX.Element;\nfunction Terminal(props: TerminalSingleProps): React.JSX.Element;\nfunction Terminal(props: TerminalProps) {\n  if (props.commands !== undefined) {\n    return <TerminalVariants {...props} />;\n  }\n\n  return <TerminalSingle {...props} />;\n}\n\nfunction TerminalSingle({\n  command,\n  label = \"Terminal\",\n  prompt = \"$\",\n  copyable = true,\n  language = \"shell\",\n  className,\n  ...props\n}: TerminalSingleProps) {\n  return (\n    <div\n      data-slot=\"terminal\"\n      className={cn(\"border border-border bg-background\", className)}\n      {...props}\n    >\n      <TerminalHeader label={label}>\n        {copyable ? <TerminalCopyButton value={command} /> : null}\n      </TerminalHeader>\n      <TerminalCode command={command} prompt={prompt} language={language} />\n    </div>\n  );\n}\n\nfunction TerminalVariants<const TCommands extends TerminalCommandList>({\n  commands,\n  defaultValue,\n  value,\n  onValueChange,\n  label = \"Terminal\",\n  prompt = \"$\",\n  copyable = true,\n  language = \"shell\",\n  className,\n  ...props\n}: TerminalVariantsProps<TCommands>) {\n  const firstValue = commands[0].value;\n  const initialValue = commands.some((item) => item.value === defaultValue)\n    ? (defaultValue ?? firstValue)\n    : firstValue;\n  const [internalValue, setInternalValue] = React.useState(initialValue);\n  const selectedValue = value ?? internalValue;\n  const selectedCommand = commands.find((item) => item.value === selectedValue) ?? commands[0];\n\n  function handleValueChange(nextValue: string) {\n    if (value === undefined) {\n      setInternalValue(nextValue);\n    }\n    onValueChange?.(nextValue);\n  }\n\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"terminal\"\n      value={selectedValue}\n      onValueChange={handleValueChange}\n      className={cn(\"border border-border bg-background\", className)}\n      {...props}\n    >\n      <TerminalHeader label={label}>\n        <TabsPrimitive.List\n          data-slot=\"terminal-list\"\n          activateOnFocus\n          aria-label=\"Command variants\"\n          className=\"flex min-w-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n        >\n          {commands.map((item) => (\n            <TabsPrimitive.Tab\n              key={item.value}\n              data-slot=\"terminal-trigger\"\n              value={item.value}\n              className=\"inline-flex min-h-11 flex-none cursor-pointer items-center border-l border-border px-3 font-mono text-[0.6875rem] uppercase tracking-[0.12em] text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted/60 hover:text-foreground focus-visible:z-10 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 data-active:bg-foreground data-active:text-background motion-reduce:transition-none\"\n            >\n              {item.label}\n            </TabsPrimitive.Tab>\n          ))}\n        </TabsPrimitive.List>\n        {copyable ? (\n          <TerminalCopyButton key={selectedCommand.value} value={selectedCommand.command} />\n        ) : null}\n      </TerminalHeader>\n\n      {commands.map((item) => (\n        <TabsPrimitive.Panel\n          key={item.value}\n          data-slot=\"terminal-panel\"\n          value={item.value}\n          className=\"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40\"\n        >\n          <TerminalCode\n            command={item.command}\n            prompt={prompt}\n            language={item.language ?? language}\n          />\n        </TabsPrimitive.Panel>\n      ))}\n    </TabsPrimitive.Root>\n  );\n}\n\nfunction TerminalHeader({\n  label,\n  children,\n}: {\n  label: React.ReactNode;\n  children?: React.ReactNode;\n}) {\n  return (\n    <div\n      data-slot=\"terminal-header\"\n      className=\"flex min-h-11 min-w-0 items-stretch border-b border-border bg-muted/20\"\n    >\n      <div className=\"flex min-w-0 flex-1 items-center gap-2 px-3 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-muted-foreground\">\n        <span className=\"truncate\">{label}</span>\n      </div>\n      <div className=\"flex min-w-0 max-w-[75%] items-stretch\">{children}</div>\n    </div>\n  );\n}\n\nfunction TerminalCode({\n  command,\n  prompt,\n  language,\n}: {\n  command: string;\n  prompt: React.ReactNode | false;\n  language: string;\n}) {\n  return (\n    <div data-slot=\"terminal-content\" className=\"min-w-0 overflow-x-auto px-4 py-4 sm:px-5\">\n      <pre\n        className={cn(\n          \"min-w-max font-mono text-[0.8125rem] leading-6 text-foreground\",\n          prompt !== false && \"grid grid-cols-[auto_1fr] items-start gap-3\",\n        )}\n      >\n        {prompt !== false ? (\n          <span className=\"select-none text-muted-foreground\" aria-hidden=\"true\">\n            {prompt}\n          </span>\n        ) : null}\n        <SyntaxHighlight code={command} language={language} />\n      </pre>\n    </div>\n  );\n}\n\nfunction TerminalCopyButton({ value }: { value: string }) {\n  const [copied, setCopied] = React.useState(false);\n\n  async function copyCommand() {\n    await navigator.clipboard.writeText(value);\n    setCopied(true);\n    window.setTimeout(() => setCopied(false), 1600);\n  }\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"terminal-copy\"\n      onClick={copyCommand}\n      className=\"inline-flex size-11 shrink-0 cursor-pointer items-center justify-center border-l border-border text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted/60 hover:text-foreground focus-visible:z-10 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 motion-reduce:transition-none\"\n      aria-label={copied ? \"Command copied\" : \"Copy command\"}\n    >\n      {copied ? (\n        <Check className=\"size-3.5\" aria-hidden=\"true\" />\n      ) : (\n        <Copy className=\"size-3.5\" aria-hidden=\"true\" />\n      )}\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {copied ? \"Command copied to clipboard\" : \"\"}\n      </span>\n    </button>\n  );\n}\n\nexport type { TerminalCommand, TerminalProps };\nexport { Terminal };\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/syntax-highlight.tsx",
      "content": "import {\n  Highlight,\n  normalizeTokens,\n  Prism,\n  type PrismGrammar,\n  type PrismTheme,\n  type Token as SyntaxToken,\n} from \"prism-react-renderer\";\n\nconst languageAliases: Record<string, string> = {\n  html: \"markup\",\n  js: \"javascript\",\n  jsx: \"jsx\",\n  md: \"markdown\",\n  sh: \"bash\",\n  shell: \"bash\",\n  ts: \"typescript\",\n  tsx: \"tsx\",\n  xml: \"markup\",\n  yml: \"yaml\",\n};\n\nconst shellGrammar: PrismGrammar = {\n  comment: {\n    pattern: /(^|[^\\\\])#.*/,\n    lookbehind: true,\n    greedy: true,\n  },\n  string: [\n    {\n      pattern: /\"(?:\\\\[\\s\\S]|[^\"\\\\])*\"/,\n      greedy: true,\n    },\n    {\n      pattern: /'(?:[^']*)'/,\n      greedy: true,\n    },\n  ],\n  variable: [\n    /\\$(?:[a-z_][\\w]*|\\d+|[?$!#@*-])/i,\n    {\n      pattern: /\\$\\{[^}]+\\}/,\n      greedy: true,\n    },\n  ],\n  option: {\n    pattern: /(^|\\s)--?[\\w-]+(?:=[^\\s]+)?/,\n    lookbehind: true,\n    alias: \"attr-name\",\n  },\n  command: {\n    pattern: /(^|[\\n;|&]\\s*)[\\w@./-]+/m,\n    lookbehind: true,\n    alias: \"function\",\n  },\n  symbol: /@[\\w./-]+/,\n  operator: /&&|\\|\\||[|;&]|>>?|<<?/,\n  punctuation: /[()[\\]{}]/,\n};\n\nif (!Prism.languages.bash) {\n  Prism.languages.bash = shellGrammar;\n}\n\nconst rivetSyntaxThemeClassName = [\n  \"[--syntax-comment:#6f7378]\",\n  \"[--syntax-foreground:#24292e]\",\n  \"[--syntax-function:#6f42c1]\",\n  \"[--syntax-keyword:#d32f2f]\",\n  \"[--syntax-number:#1976d2]\",\n  \"[--syntax-punctuation:#212121]\",\n  \"[--syntax-string:#22863a]\",\n  \"[--syntax-text:#24292e]\",\n  \"[--syntax-variable:#1976d2]\",\n  \"dark:[--syntax-comment:#8b949e]\",\n  \"dark:[--syntax-foreground:#b392f0]\",\n  \"dark:[--syntax-function:#b392f0]\",\n  \"dark:[--syntax-keyword:#f97583]\",\n  \"dark:[--syntax-number:#f8f8f8]\",\n  \"dark:[--syntax-punctuation:#bbbbbb]\",\n  \"dark:[--syntax-string:#ffab70]\",\n  \"dark:[--syntax-text:#f8f8f8]\",\n  \"dark:[--syntax-variable:#79b8ff]\",\n].join(\" \");\n\nconst rivetSyntaxTheme: PrismTheme = {\n  plain: {\n    color: \"var(--syntax-foreground)\",\n  },\n  styles: [\n    {\n      types: [\"comment\", \"prolog\", \"doctype\", \"cdata\"],\n      style: {\n        color: \"var(--syntax-comment)\",\n        fontStyle: \"italic\",\n      },\n    },\n    {\n      types: [\"punctuation\", \"operator\", \"entity\", \"url\"],\n      style: {\n        color: \"var(--syntax-punctuation)\",\n      },\n    },\n    {\n      types: [\"string\", \"char\", \"attr-value\", \"regex\", \"inserted\"],\n      style: {\n        color: \"var(--syntax-string)\",\n      },\n    },\n    {\n      types: [\"keyword\", \"module\", \"control-flow\", \"important\", \"atrule\"],\n      style: {\n        color: \"var(--syntax-keyword)\",\n      },\n    },\n    {\n      types: [\"function\", \"function-variable\", \"class-name\", \"builtin\", \"property\", \"tag\"],\n      style: {\n        color: \"var(--syntax-function)\",\n      },\n    },\n    {\n      types: [\"attr-name\", \"variable\"],\n      style: {\n        color: \"var(--syntax-variable)\",\n      },\n    },\n    {\n      types: [\"boolean\", \"number\", \"constant\", \"symbol\"],\n      style: {\n        color: \"var(--syntax-number)\",\n      },\n    },\n    {\n      types: [\"deleted\"],\n      style: {\n        color: \"var(--destructive)\",\n      },\n    },\n  ],\n};\n\ntype SyntaxHighlightProps = {\n  code: string;\n  language?: string;\n  showLineNumbers?: boolean;\n  highlightLinesOnHover?: boolean;\n  softWrap?: boolean;\n};\n\nfunction normalizeLanguage(language: string | undefined) {\n  const normalizedLanguage = language?.trim().toLowerCase() || \"text\";\n\n  return languageAliases[normalizedLanguage] ?? normalizedLanguage;\n}\n\nfunction getLineIds(code: string) {\n  const occurrences = new Map<string, number>();\n\n  return code.split(\"\\n\").map((content) => {\n    const occurrence = (occurrences.get(content) ?? 0) + 1;\n    occurrences.set(content, occurrence);\n\n    return `${content}-${occurrence}`;\n  });\n}\n\nfunction findJsxTagEnd(content: string) {\n  let braceDepth = 0;\n  let quote: '\"' | \"'\" | null = null;\n  let isEscaped = false;\n\n  for (let index = 1; index < content.length; index += 1) {\n    const character = content[index];\n\n    if (quote) {\n      if (isEscaped) {\n        isEscaped = false;\n      } else if (character === \"\\\\\") {\n        isEscaped = true;\n      } else if (character === quote) {\n        quote = null;\n      }\n\n      continue;\n    }\n\n    if (character === '\"' || character === \"'\") {\n      quote = character;\n    } else if (character === \"{\") {\n      braceDepth += 1;\n    } else if (character === \"}\") {\n      braceDepth = Math.max(0, braceDepth - 1);\n    } else if (character === \">\" && braceDepth === 0) {\n      return index;\n    }\n  }\n\n  return -1;\n}\n\nfunction normalizeEmbeddedJsxTags(tokens: SyntaxToken[][], language: string) {\n  if (language !== \"jsx\" && language !== \"tsx\") {\n    return tokens;\n  }\n\n  return tokens.map((line) =>\n    line.flatMap((token) => {\n      if (!token.types.includes(\"plain-text\") || !/^<[A-Za-z]/.test(token.content)) {\n        return token;\n      }\n\n      const normalized: SyntaxToken[] = [];\n      let remainingContent = token.content;\n\n      while (/^<[A-Za-z]/.test(remainingContent)) {\n        const tagEnd = findJsxTagEnd(remainingContent);\n\n        if (tagEnd === -1) {\n          break;\n        }\n\n        const tag = remainingContent.slice(0, tagEnd + 1);\n        const tagTokens = normalizeTokens(Prism.tokenize(tag, Prism.languages.jsx)).flat();\n        normalized.push(...tagTokens);\n        remainingContent = remainingContent.slice(tagEnd + 1);\n      }\n\n      if (remainingContent) {\n        normalized.push({ ...token, content: remainingContent });\n      }\n\n      return normalized.length > 0 ? normalized : token;\n    }),\n  );\n}\n\nfunction getJsxTextTokenKeys(tokens: SyntaxToken[][], language: string) {\n  const textTokenKeys = new Set<string>();\n\n  if (language !== \"jsx\" && language !== \"tsx\") {\n    return textTokenKeys;\n  }\n\n  let jsxDepth = 0;\n  let expressionDepth = 0;\n  let isInsideTag = false;\n  let isClosingTag = false;\n\n  tokens.forEach((line, lineIndex) => {\n    line.forEach((token, tokenIndex) => {\n      const { content, types } = token;\n\n      if (isInsideTag) {\n        if (content.includes(\"/>\")) {\n          isInsideTag = false;\n          isClosingTag = false;\n          return;\n        }\n\n        if (content.includes(\">\")) {\n          jsxDepth = isClosingTag ? Math.max(0, jsxDepth - 1) : jsxDepth + 1;\n          isInsideTag = false;\n          isClosingTag = false;\n        }\n\n        return;\n      }\n\n      if (expressionDepth > 0) {\n        expressionDepth += (content.match(/\\{/g) ?? []).length;\n        expressionDepth -= (content.match(/\\}/g) ?? []).length;\n        return;\n      }\n\n      if (jsxDepth > 0 && content.includes(\"{\")) {\n        expressionDepth = (content.match(/\\{/g) ?? []).length - (content.match(/\\}/g) ?? []).length;\n        return;\n      }\n\n      const startsTag =\n        (content === \"<\" || content === \"</\") && (types.includes(\"tag\") || jsxDepth > 0);\n\n      if (startsTag) {\n        isInsideTag = true;\n        isClosingTag = content === \"</\";\n        return;\n      }\n\n      if (jsxDepth > 0 && content.trim()) {\n        textTokenKeys.add(`${lineIndex}:${tokenIndex}`);\n      }\n    });\n  });\n\n  return textTokenKeys;\n}\n\nfunction SyntaxHighlight({\n  code,\n  language,\n  showLineNumbers = false,\n  highlightLinesOnHover = false,\n  softWrap = false,\n}: SyntaxHighlightProps) {\n  const normalizedLanguage = normalizeLanguage(language);\n  const lineIds = getLineIds(code);\n  const lineNumberWidth = `calc(${String(lineIds.length).length}ch + 1.5rem)`;\n\n  return (\n    <Highlight code={code} language={normalizedLanguage} theme={rivetSyntaxTheme}>\n      {({ tokens, getLineProps, getTokenProps }) => {\n        const normalizedTokens = normalizeEmbeddedJsxTags(tokens, normalizedLanguage);\n        const jsxTextTokenKeys = getJsxTextTokenKeys(normalizedTokens, normalizedLanguage);\n\n        return (\n          <code\n            data-language={language}\n            className={`language-${normalizedLanguage} ${rivetSyntaxThemeClassName} ${\n              softWrap ? \"block min-w-0 whitespace-pre-wrap wrap-anywhere\" : \"\"\n            }`}\n          >\n            {normalizedTokens.map((line, lineIndex) => {\n              const lineProps = getLineProps({ line });\n              const tokenOccurrences = new Map<string, number>();\n              const content = line.map((token, tokenIndex) => {\n                const tokenProps = getTokenProps({ token });\n                const signature = `${token.types.join(\":\")}-${token.content}`;\n                const occurrence = (tokenOccurrences.get(signature) ?? 0) + 1;\n                tokenOccurrences.set(signature, occurrence);\n\n                if (jsxTextTokenKeys.has(`${lineIndex}:${tokenIndex}`)) {\n                  tokenProps.style = {\n                    ...tokenProps.style,\n                    color: \"var(--syntax-text)\",\n                  };\n                }\n\n                return <span {...tokenProps} key={`${signature}-${occurrence}`} />;\n              });\n\n              if (showLineNumbers || highlightLinesOnHover) {\n                return (\n                  <span\n                    {...lineProps}\n                    key={lineIds[lineIndex]}\n                    className={`${lineProps.className} flex w-full ${\n                      softWrap ? \"min-w-0\" : \"min-w-max\"\n                    } ${\n                      highlightLinesOnHover\n                        ? \"transition-colors duration-100 hover:bg-muted/50 motion-reduce:transition-none\"\n                        : \"\"\n                    }`}\n                  >\n                    {showLineNumbers ? (\n                      <span\n                        key=\"line-number\"\n                        aria-hidden=\"true\"\n                        className=\"shrink-0 select-none border-r border-border pr-2 pl-4 text-right tabular-nums text-muted-foreground/60\"\n                        style={{ width: lineNumberWidth }}\n                      >\n                        {lineIndex + 1}\n                      </span>\n                    ) : null}\n                    <span\n                      key=\"line-content\"\n                      className={`px-4 ${\n                        softWrap\n                          ? \"min-w-0 flex-1 whitespace-pre-wrap wrap-anywhere\"\n                          : \"min-w-max flex-1 whitespace-pre\"\n                      }`}\n                    >\n                      {content}\n                    </span>\n                  </span>\n                );\n              }\n\n              return (\n                <span {...lineProps} key={lineIds[lineIndex]}>\n                  {content}\n                  {lineIndex < normalizedTokens.length - 1 ? \"\\n\" : null}\n                </span>\n              );\n            })}\n          </code>\n        );\n      }}\n    </Highlight>\n  );\n}\n\nexport type { SyntaxHighlightProps };\nexport { SyntaxHighlight };\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}