{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-tabs",
  "title": "Command Tabs",
  "description": "A copyable command block with attached agent tabs and a sliding active pill \u2014 tabs can sit above or below the command.",
  "dependencies": ["@phosphor-icons/react"],
  "registryDependencies": ["button"],
  "files": [
    {
      "path": "components/ui/command-tabs.tsx",
      "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon } from \"@phosphor-icons/react\";\nimport {\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n  type PointerEvent as ReactPointerEvent,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ninterface CommandTabsItem {\n  value: string;\n  label: string;\n  command: string;\n  icon?: ReactNode;\n}\n\ninterface CommandTabsProps {\n  items: CommandTabsItem[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  tabsPosition?: \"top\" | \"bottom\" | \"none\";\n  invertActiveIcon?: boolean;\n  highlight?: boolean;\n  label?: string;\n  copyLabel?: string;\n  onCopy?: (command: string) => void;\n  className?: string;\n}\n\nconst COPY_FEEDBACK_MS = 2000;\nconst SCROLLBAR_HIDE_MS = 800;\nconst PERCENT = 100;\nconst SCROLL_STEP_PX = 40;\n\ntype CommandTokenType = \"command\" | \"string\" | \"flag\" | \"plain\";\n\ninterface CommandToken {\n  text: string;\n  type: CommandTokenType;\n  start: number;\n}\n\nconst COMMAND_TOKEN_RE = /\"[^\"]*\"?|'[^']*'?|\\s+|[^\\s\"']+/g;\nconst WHITESPACE_RE = /^\\s+$/;\n\nconst TOKEN_CLASS: Record<CommandTokenType, string | undefined> = {\n  command: \"text-sky-600 dark:text-sky-400\",\n  string: \"text-emerald-600 dark:text-emerald-400\",\n  flag: \"text-muted-foreground\",\n  plain: undefined,\n};\n\nfunction classifyToken(text: string, isFirstWord: boolean): CommandTokenType {\n  if (text.startsWith('\"') || text.startsWith(\"'\")) {\n    return \"string\";\n  }\n  if (text.startsWith(\"-\")) {\n    return \"flag\";\n  }\n  return isFirstWord ? \"command\" : \"plain\";\n}\n\nfunction tokenizeCommand(command: string): CommandToken[] {\n  const tokens: CommandToken[] = [];\n  let firstWord = true;\n  let start = 0;\n  for (const text of command.match(COMMAND_TOKEN_RE) ?? []) {\n    if (WHITESPACE_RE.test(text)) {\n      tokens.push({ text, type: \"plain\", start });\n    } else {\n      tokens.push({ text, type: classifyToken(text, firstWord), start });\n      firstWord = false;\n    }\n    start += text.length;\n  }\n  return tokens;\n}\n\nfunction HighlightedCommand({ command }: { command: string }) {\n  return tokenizeCommand(command).map((token) => (\n    <span className={TOKEN_CLASS[token.type]} key={token.start}>\n      {token.text}\n    </span>\n  ));\n}\n\ninterface CommandTabProps {\n  item: CommandTabsItem;\n  isActive: boolean;\n  invertIcon: boolean;\n  onSelect: (value: string) => void;\n}\n\nfunction CommandTab({ item, isActive, invertIcon, onSelect }: CommandTabProps) {\n  return (\n    <button\n      aria-pressed={isActive}\n      className={cn(\n        \"relative z-10 flex shrink-0 cursor-pointer items-center gap-2 rounded-[calc(var(--radius-xl)-6px)] px-3 py-1.5 font-medium text-sm outline-none transition-colors duration-300 ease-out focus-visible:ring-2 focus-visible:ring-ring\",\n        isActive\n          ? \"text-primary-foreground dark:text-white\"\n          : \"text-muted-foreground hover:text-foreground\"\n      )}\n      data-value={item.value}\n      onClick={() => onSelect(item.value)}\n      type=\"button\"\n    >\n      {item.icon && (\n        <span\n          className={cn(\n            \"flex shrink-0 items-center [&_svg]:size-4\",\n            isActive &&\n              invertIcon &&\n              \"[&_svg]:fill-current [&_svg_path]:fill-current\"\n          )}\n        >\n          {item.icon}\n        </span>\n      )}\n      <span className=\"whitespace-nowrap\">{item.label}</span>\n    </button>\n  );\n}\n\nfunction useActivePill(activeValue: string | undefined) {\n  const menuRef = useRef<HTMLMenuElement>(null);\n  const [pill, setPill] = useState<{ left: number; width: number } | null>(\n    null\n  );\n\n  const measure = useCallback(() => {\n    const menu = menuRef.current;\n    if (!menu || activeValue === undefined) {\n      setPill(null);\n      return;\n    }\n    const active = menu.querySelector<HTMLButtonElement>(\n      `[data-value=\"${CSS.escape(activeValue)}\"]`\n    );\n    if (!active) {\n      setPill(null);\n      return;\n    }\n    setPill({ left: active.offsetLeft, width: active.offsetWidth });\n  }, [activeValue]);\n\n  useLayoutEffect(() => {\n    measure();\n  }, [measure]);\n\n  useEffect(() => {\n    const menu = menuRef.current;\n    if (!menu) {\n      return;\n    }\n    const observer = new ResizeObserver(measure);\n    observer.observe(menu);\n    for (const tab of menu.querySelectorAll(\"[data-value]\")) {\n      observer.observe(tab);\n    }\n    return () => observer.disconnect();\n  }, [measure]);\n\n  return { menuRef, pill };\n}\n\nfunction useFloatingScrollbar(activeCommand: string | undefined) {\n  const scrollRef = useRef<HTMLElement>(null);\n  const hideTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const [thumb, setThumb] = useState({ widthPct: 0, leftPct: 0 });\n  const [scrolling, setScrolling] = useState(false);\n\n  const measure = useCallback(() => {\n    const el = scrollRef.current;\n    if (!el) {\n      return;\n    }\n    const { scrollWidth, clientWidth, scrollLeft } = el;\n    if (scrollWidth <= clientWidth) {\n      setThumb({ widthPct: 0, leftPct: 0 });\n      return;\n    }\n    setThumb({\n      widthPct: (clientWidth / scrollWidth) * PERCENT,\n      leftPct: (scrollLeft / scrollWidth) * PERCENT,\n    });\n  }, []);\n\n  const handleScroll = useCallback(() => {\n    measure();\n    setScrolling(true);\n    if (hideTimeout.current) {\n      clearTimeout(hideTimeout.current);\n    }\n    hideTimeout.current = setTimeout(\n      () => setScrolling(false),\n      SCROLLBAR_HIDE_MS\n    );\n  }, [measure]);\n\n  useEffect(() => {\n    const el = scrollRef.current;\n    if (!el) {\n      return;\n    }\n    const observer = new ResizeObserver(measure);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [measure]);\n\n  useEffect(() => {\n    const el = scrollRef.current;\n    if (el && activeCommand !== undefined) {\n      el.scrollLeft = 0;\n    }\n    measure();\n  }, [measure, activeCommand]);\n\n  useEffect(\n    () => () => {\n      if (hideTimeout.current) {\n        clearTimeout(hideTimeout.current);\n      }\n    },\n    []\n  );\n\n  const dragStart = useRef<{ x: number; scrollLeft: number } | null>(null);\n  const [dragging, setDragging] = useState(false);\n\n  const handleThumbPointerDown = useCallback(\n    (event: ReactPointerEvent<HTMLElement>) => {\n      const el = scrollRef.current;\n      if (!el) {\n        return;\n      }\n      event.preventDefault();\n      event.currentTarget.setPointerCapture(event.pointerId);\n      dragStart.current = { x: event.clientX, scrollLeft: el.scrollLeft };\n      setDragging(true);\n    },\n    []\n  );\n\n  const handleThumbPointerMove = useCallback(\n    (event: ReactPointerEvent<HTMLElement>) => {\n      const el = scrollRef.current;\n      const start = dragStart.current;\n      if (!(el && start)) {\n        return;\n      }\n      const delta = event.clientX - start.x;\n      el.scrollLeft =\n        start.scrollLeft + delta * (el.scrollWidth / el.clientWidth);\n    },\n    []\n  );\n\n  const handleThumbPointerEnd = useCallback(() => {\n    dragStart.current = null;\n    setDragging(false);\n  }, []);\n\n  const handleThumbKeyDown = useCallback(\n    (event: ReactKeyboardEvent<HTMLElement>) => {\n      const el = scrollRef.current;\n      if (!el) {\n        return;\n      }\n      if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        el.scrollLeft -= SCROLL_STEP_PX;\n      } else if (event.key === \"ArrowRight\") {\n        event.preventDefault();\n        el.scrollLeft += SCROLL_STEP_PX;\n      }\n    },\n    []\n  );\n\n  return {\n    scrollRef,\n    thumb,\n    visible: scrolling || dragging,\n    dragging,\n    handleScroll,\n    handleThumbPointerDown,\n    handleThumbPointerMove,\n    handleThumbPointerEnd,\n    handleThumbKeyDown,\n  };\n}\n\ninterface ScrollbarThumbProps {\n  controls: string;\n  thumb: { widthPct: number; leftPct: number };\n  visible: boolean;\n  dragging: boolean;\n  onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => void;\n  onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\n  onPointerMove: (event: ReactPointerEvent<HTMLElement>) => void;\n  onPointerEnd: () => void;\n}\n\nfunction ScrollbarThumb({\n  controls,\n  thumb,\n  visible,\n  dragging,\n  onKeyDown,\n  onPointerDown,\n  onPointerMove,\n  onPointerEnd,\n}: ScrollbarThumbProps) {\n  const scrollableTrackPct = PERCENT - thumb.widthPct;\n  const valueNow =\n    scrollableTrackPct > 0\n      ? Math.round((thumb.leftPct / scrollableTrackPct) * PERCENT)\n      : 0;\n\n  return (\n    <div\n      aria-controls={controls}\n      aria-label=\"Scroll command\"\n      aria-orientation=\"horizontal\"\n      aria-valuemax={PERCENT}\n      aria-valuemin={0}\n      aria-valuenow={valueNow}\n      className={cn(\n        \"absolute bottom-0 flex h-2.5 cursor-grab touch-none items-end transition-opacity duration-300\",\n        \"focus-visible:opacity-100 focus-visible:outline-none group-hover:opacity-100\",\n        dragging && \"cursor-grabbing\",\n        visible ? \"opacity-100\" : \"opacity-0\"\n      )}\n      onKeyDown={onKeyDown}\n      onPointerCancel={onPointerEnd}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerEnd}\n      role=\"scrollbar\"\n      style={{\n        width: `${thumb.widthPct}%`,\n        left: `${thumb.leftPct}%`,\n      }}\n      tabIndex={0}\n    >\n      <span\n        className={cn(\n          \"h-1 w-full rounded-full bg-foreground/25\",\n          dragging && \"bg-foreground/40\"\n        )}\n      />\n    </div>\n  );\n}\n\nexport function CommandTabs({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  tabsPosition = \"bottom\",\n  invertActiveIcon = true,\n  highlight = false,\n  label = \"Choose a tool\",\n  copyLabel = \"Copy\",\n  onCopy,\n  className,\n}: CommandTabsProps) {\n  const [internalValue, setInternalValue] = useState(\n    defaultValue ?? items[0]?.value\n  );\n  const [copied, setCopied] = useState(false);\n  const copyTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const activeValue = value ?? internalValue;\n  const activeItem = items.find((item) => item.value === activeValue);\n  const { menuRef, pill } = useActivePill(\n    tabsPosition === \"none\" ? undefined : activeValue\n  );\n  const {\n    scrollRef,\n    thumb,\n    visible,\n    dragging,\n    handleScroll,\n    handleThumbPointerDown,\n    handleThumbPointerMove,\n    handleThumbPointerEnd,\n    handleThumbKeyDown,\n  } = useFloatingScrollbar(activeItem?.command);\n  const commandId = useId();\n\n  useEffect(\n    () => () => {\n      if (copyTimeout.current) {\n        clearTimeout(copyTimeout.current);\n      }\n    },\n    []\n  );\n\n  const handleSelect = (next: string) => {\n    setInternalValue(next);\n    onValueChange?.(next);\n  };\n\n  const handleCopy = async () => {\n    if (!activeItem) {\n      return;\n    }\n    try {\n      await navigator.clipboard.writeText(activeItem.command);\n      onCopy?.(activeItem.command);\n      setCopied(true);\n      if (copyTimeout.current) {\n        clearTimeout(copyTimeout.current);\n      }\n      copyTimeout.current = setTimeout(\n        () => setCopied(false),\n        COPY_FEEDBACK_MS\n      );\n    } catch {\n      setCopied(false);\n    }\n  };\n\n  return (\n    <div className={cn(\"flex w-full flex-col\", className)}>\n      <div\n        className={cn(\n          \"relative z-10 flex items-center gap-2 rounded-xl border bg-muted/50 py-1.5 pr-1.5 pl-4\",\n          tabsPosition === \"top\" && \"order-last\"\n        )}\n      >\n        <div className=\"group relative min-w-0 flex-1\">\n          <code\n            className=\"block overflow-x-auto whitespace-nowrap py-1.5 font-mono text-foreground text-sm [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n            id={commandId}\n            onScroll={handleScroll}\n            ref={scrollRef}\n          >\n            {activeItem &&\n              (highlight ? (\n                <HighlightedCommand command={activeItem.command} />\n              ) : (\n                activeItem.command\n              ))}\n          </code>\n          {thumb.widthPct > 0 && (\n            <ScrollbarThumb\n              controls={commandId}\n              dragging={dragging}\n              onKeyDown={handleThumbKeyDown}\n              onPointerDown={handleThumbPointerDown}\n              onPointerEnd={handleThumbPointerEnd}\n              onPointerMove={handleThumbPointerMove}\n              thumb={thumb}\n              visible={visible}\n            />\n          )}\n        </div>\n        <Button\n          className=\"shrink-0 cursor-pointer\"\n          onClick={handleCopy}\n          size=\"sm\"\n          variant=\"outline\"\n        >\n          {copied ? <CheckIcon /> : <CopyIcon />}\n          {copied ? \"Copied\" : copyLabel}\n        </Button>\n      </div>\n      {tabsPosition !== \"none\" && (\n        <menu\n          aria-label={label}\n          className={cn(\n            \"relative m-0 flex w-fit max-w-[calc(100%-2rem)] list-none items-center gap-1 self-start overflow-x-auto overscroll-none border bg-muted/50 p-1.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n            tabsPosition === \"bottom\"\n              ? \"order-last -mt-px ml-4 rounded-b-xl border-t-0\"\n              : \"order-first -mb-px ml-4 rounded-t-xl border-b-0\"\n          )}\n          ref={menuRef}\n        >\n          {pill && (\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-y-1.5 rounded-[calc(var(--radius-xl)-6px)] bg-primary shadow-sm transition-[left,width] duration-300 ease-out\"\n              style={{ left: pill.left, width: pill.width }}\n            />\n          )}\n          {items.map((item) => (\n            <CommandTab\n              invertIcon={invertActiveIcon}\n              isActive={item.value === activeValue}\n              item={item}\n              key={item.value}\n              onSelect={handleSelect}\n            />\n          ))}\n        </menu>\n      )}\n    </div>\n  );\n}\n\nexport type { CommandTabsItem, CommandTabsProps };\n",
      "type": "registry:ui",
      "target": "@ui/command-tabs.tsx"
    }
  ],
  "type": "registry:ui"
}
