{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-bar",
  "title": "Command Bar",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A command palette with pages: a search-field style trigger, grouped commands, drill-in pages with a breadcrumb, Backspace to go back and a footer of key hints.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/command.json",
    "https://ui.ballmac.com/r/kbd.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/command-bar.tsx",
      "content": "// Ballmac UI: Command Bar. https://ui.ballmac.com/components/command-bar\n\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronRight, CornerDownLeft, Search } from \"lucide-react\";\nimport {\n  CommandDialog,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandShortcut,\n} from \"@/components/ballmac/command\";\nimport { Kbd } from \"@/components/ballmac/kbd\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype CommandBarItem = {\n  /** Unique id. */\n  id: string;\n  /** Visible name. */\n  label: string;\n  /** Muted second line. */\n  description?: string;\n  /** Leading icon. */\n  icon?: React.ReactNode;\n  /** Keys shown at the end, for example `[\"⌘\", \"N\"]`. Display only. */\n  shortcut?: string[];\n  /** Extra words the search should match. */\n  keywords?: string[];\n  /** Runs when chosen. The bar closes afterwards unless `keepOpen` is set. */\n  onSelect?: () => void;\n  /** Keep the bar open after running `onSelect`. */\n  keepOpen?: boolean;\n  /** Choosing this item opens a second page with these groups (for example \"Change theme…\"). */\n  pages?: CommandBarGroup[];\n};\n\ntype CommandBarGroup = {\n  /** Group heading. */\n  heading: string;\n  items: CommandBarItem[];\n};\n\ntype CommandBarProps = {\n  /** Top-level groups. */\n  groups: CommandBarGroup[];\n  /** Controlled open state. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean;\n  /** Called when the bar opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Placeholder of the search field on the first page. */\n  placeholder?: string;\n  /** Text shown when nothing matches. */\n  emptyText?: string;\n  /** Key that toggles the bar together with ⌘ (Ctrl on Windows and Linux). `false` turns the shortcut off. */\n  hotkey?: string | false;\n  /** Text on the trigger button. `false` hides the trigger so you can open the bar yourself. */\n  trigger?: string | false;\n  /** Classes for the trigger button. */\n  triggerClassName?: string;\n  /** Accessible name of the dialog. */\n  title?: string;\n};\n\n/**\n * A command palette with pages. Type to search; choose an item that has `pages` to drill in (Backspace on an empty\n * field goes back); every other item runs and closes. The trigger is a search-field-style button with the hotkey.\n */\nfunction CommandBar({\n  groups,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  placeholder,\n  emptyText,\n  hotkey = \"k\",\n  trigger = \"Search or jump to…\",\n  triggerClassName,\n  title,\n}: CommandBarProps) {\n  const msg = useMessages()\n  placeholder ??= msg(\"command-bar.placeholder\", \"Search commands…\")\n  emptyText ??= msg(\"command-bar.emptyText\", \"No results found.\")\n  title ??= msg(\"command-bar.title\", \"Command bar\")\n  const [inner, setInner] = React.useState(defaultOpen);\n  const open = openProp ?? inner;\n  const [stack, setStack] = React.useState<CommandBarItem[]>([]);\n  const [search, setSearch] = React.useState(\"\");\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (openProp === undefined) setInner(next);\n      onOpenChange?.(next);\n      if (!next) {\n        setStack([]);\n        setSearch(\"\");\n      }\n    },\n    [openProp, onOpenChange],\n  );\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const current = stack.at(-1);\n  const visibleGroups = current?.pages ?? groups;\n\n  function choose(item: CommandBarItem) {\n    if (item.pages) {\n      setStack((s) => [...s, item]);\n      setSearch(\"\");\n      // Choosing with the pointer moves focus off the field; bring it back so typing and Backspace keep working.\n      requestAnimationFrame(() => inputRef.current?.focus());\n      return;\n    }\n    item.onSelect?.();\n    if (!item.keepOpen) setOpen(false);\n  }\n\n  return (\n    <>\n      {trigger !== false && (\n        <button\n          type=\"button\"\n          data-slot=\"command-bar-trigger\"\n          onClick={() => setOpen(true)}\n          aria-haspopup=\"dialog\"\n          aria-keyshortcuts={hotkey ? `Meta+${hotkey.toUpperCase()} Control+${hotkey.toUpperCase()}` : undefined}\n          className={cn(\n            \"inline-flex h-10 w-full max-w-sm items-center gap-2.5 rounded-xl border bg-background px-3 text-start text-sm text-muted-foreground shadow-xs outline-none transition-[border-color,box-shadow,background-color] hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n            triggerClassName,\n          )}\n        >\n          <Search aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n          <span className=\"flex-1 truncate\">{trigger}</span>\n          {hotkey && (\n            <span aria-hidden=\"true\" className=\"flex items-center gap-1\">\n              <Kbd size=\"sm\">⌘</Kbd>\n              <Kbd size=\"sm\">{hotkey.toUpperCase()}</Kbd>\n            </span>\n          )}\n        </button>\n      )}\n      <CommandDialog\n        open={open}\n        onOpenChange={setOpen}\n        hotkey={hotkey}\n        title={title}\n        label={title}\n        className=\"[&_[cmdk-list]]:max-h-[min(22rem,50dvh)]\"\n      >\n        {stack.length > 0 && (\n          <div className=\"flex items-center gap-1 border-b px-3 pt-2.5 pb-2 text-xs\" aria-label={msg(\"command-bar.currentPage\", \"Current page\")}>\n            <button\n              type=\"button\"\n              onClick={() => {\n                setStack([]);\n                setSearch(\"\");\n              }}\n              className=\"rounded px-1.5 py-0.5 text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            >\n              {msg(\"command-bar.all\", \"All\")}\n            </button>\n            {stack.map((page, index) => (\n              <React.Fragment key={page.id}>\n                <ChevronRight aria-hidden=\"true\" className=\"size-3 text-muted-foreground rtl:rotate-180\" />\n                <span aria-current={index === stack.length - 1 ? \"page\" : undefined} className=\"rounded bg-accent px-1.5 py-0.5 font-medium\">\n                  {page.label}\n                </span>\n              </React.Fragment>\n            ))}\n          </div>\n        )}\n        <CommandInput\n          ref={inputRef}\n          value={search}\n          onValueChange={setSearch}\n          placeholder={current ? `Search ${current.label.toLowerCase()}…` : placeholder}\n          onKeyDown={(event) => {\n            if (event.key === \"Backspace\" && !search && stack.length) {\n              event.preventDefault();\n              setStack((s) => s.slice(0, -1));\n            }\n          }}\n        />\n        <CommandList>\n          <CommandEmpty>{emptyText}</CommandEmpty>\n          {visibleGroups.map((group) => (\n            <CommandGroup key={group.heading} heading={group.heading}>\n              {group.items.map((item) => (\n                <CommandItem\n                  key={item.id}\n                  value={item.id}\n                  keywords={[item.label, ...(item.keywords ?? [])]}\n                  icon={item.icon}\n                  description={item.description}\n                  onSelect={() => choose(item)}\n                >\n                  {item.label}\n                  {item.pages ? (\n                    <ChevronRight aria-hidden=\"true\" className=\"ms-auto size-4 rtl:rotate-180\" />\n                  ) : (\n                    item.shortcut && <CommandShortcut>{item.shortcut.join(\" \")}</CommandShortcut>\n                  )}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          ))}\n        </CommandList>\n        <div\n          aria-hidden=\"true\"\n          className=\"flex items-center gap-4 border-t bg-muted/40 px-3.5 py-2 text-xs text-muted-foreground\"\n        >\n          <span className=\"flex items-center gap-1.5\">\n            <Kbd size=\"sm\">↑</Kbd>\n            <Kbd size=\"sm\">↓</Kbd> {msg(\"command-bar.navigate\", \"Navigate\")}\n          </span>\n          <span className=\"flex items-center gap-1.5\">\n            <Kbd size=\"sm\">\n              <CornerDownLeft className=\"size-3 rtl:-scale-x-100\" />\n            </Kbd>{\" \"}\n            {msg(\"command-bar.select\", \"Select\")}\n          </span>\n          {stack.length > 0 && (\n            <span className=\"flex items-center gap-1.5\">\n              <Kbd size=\"sm\">⌫</Kbd> {msg(\"command-bar.back\", \"Back\")}\n            </span>\n          )}\n          <span className=\"ms-auto flex items-center gap-1.5\">\n            <Kbd size=\"sm\">{msg(\"command-bar.esc\", \"Esc\")}</Kbd> {msg(\"command-bar.close\", \"Close\")}\n          </span>\n        </div>\n      </CommandDialog>\n    </>\n  );\n}\n\nexport { CommandBar, type CommandBarProps, type CommandBarGroup, type CommandBarItem };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/command-bar.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "command palette",
      "search",
      "pages",
      "keyboard"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "groups: {heading, items:[{id,label,icon,shortcut,keywords,onSelect,pages}]}. An item with pages opens a second page. hotkey toggles with Cmd/Ctrl; trigger={false} hides the button.",
      "whenToUse": [
        "Global quick actions and navigation",
        "Apps with many commands and sub-choices"
      ],
      "whenNotToUse": [
        "A macOS-style launcher; use spotlight-search",
        "Choosing one value in a form; use combobox"
      ],
      "composesWith": [
        "command",
        "kbd",
        "dialog"
      ],
      "a11y": [
        {
          "keys": "Cmd/Ctrl+K",
          "action": "Toggles the bar"
        },
        {
          "keys": "Type",
          "action": "Filters commands"
        },
        {
          "keys": "ArrowUp / ArrowDown / Enter",
          "action": "Moves and runs"
        },
        {
          "keys": "Backspace on an empty field",
          "action": "Goes back one page"
        },
        {
          "keys": "Escape",
          "action": "Closes"
        }
      ],
      "customization": [
        "hotkey",
        "trigger text or false",
        "pages for nested choices",
        "controlled open"
      ]
    },
    "examples": [
      "command-bar-demo",
      "command-bar-states"
    ],
    "url": "https://ui.ballmac.com/components/command-bar"
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}