{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-menu",
  "title": "CommandMenu",
  "description": "Search everything and act from the keyboard. Press ⌘K or Ctrl+K to see it in use: the search on this site is a CommandMenu.",
  "dependencies": [
    "@radix-ui/react-dialog@^1.2.0"
  ],
  "registryDependencies": [
    "https://creance.pages.dev/r/combobox.json",
    "https://creance.pages.dev/r/creance-base.json",
    "https://creance.pages.dev/r/dialog.json",
    "https://creance.pages.dev/r/kbd.json",
    "https://creance.pages.dev/r/menu.json"
  ],
  "files": [
    {
      "path": "command-menu/CommandMenu.css",
      "content": ".cr-command {\n  position: fixed;\n  top: 12vh;\n  left: 50%;\n  z-index: 41;\n  box-sizing: border-box;\n  display: flex;\n  flex-direction: column;\n  width: min(640px, calc(100vw - 2 * var(--cr-space-4)));\n  max-height: min(480px, 76vh);\n  overflow: hidden;\n  border-radius: var(--cr-radius-xl);\n  background-color: var(--cr-color-glass-popover);\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-md);\n  transform: translateX(-50%);\n  animation: cr-menu-in var(--cr-motion-duration-base) var(--cr-motion-easing-spring);\n}\n\n.cr-command:focus-visible {\n  outline: none;\n}\n\n.cr-command__search {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: var(--cr-space-4) 18px;\n  border-bottom: 1px solid var(--cr-color-border-default);\n  color: var(--cr-color-text-muted);\n}\n\n.cr-command__input {\n  flex: 1;\n  min-width: 0;\n  border: 0;\n  outline: none;\n  background: transparent;\n  color: var(--cr-color-text-default);\n  font: inherit;\n  font-size: 17px;\n}\n\n.cr-command__input::placeholder {\n  color: var(--cr-color-text-muted);\n}\n\n.cr-command__list {\n  flex: 1;\n  padding: 6px var(--cr-space-2);\n  overflow-y: auto;\n}\n\n.cr-command__group {\n  padding: var(--cr-space-2) var(--cr-space-3) var(--cr-space-1);\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n  font-weight: var(--cr-font-weight-semibold);\n}\n\n.cr-command__list .cr-combobox__option[data-tone='danger'] {\n  color: var(--cr-color-danger-text);\n}\n\n.cr-command__shortcut {\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-command__empty {\n  margin: 0;\n  padding: var(--cr-space-6) var(--cr-space-3);\n  color: var(--cr-color-text-muted);\n  text-align: center;\n}\n\n.cr-command__footer {\n  display: flex;\n  gap: var(--cr-space-4);\n  padding: 10px 18px;\n  border-top: 1px solid var(--cr-color-border-default);\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .cr-command { animation: none; }\n}\n",
      "type": "registry:file",
      "target": "@components/creance/command-menu/CommandMenu.css"
    },
    {
      "path": "command-menu/CommandMenu.tsx",
      "content": "'use client';\n\nimport * as RadixDialog from '@radix-ui/react-dialog';\nimport { useEffect, useMemo, useState, type ReactNode } from 'react';\nimport { Kbd } from '@/components/creance/kbd/Kbd';\nimport { cx } from '@/lib/creance/cx';\nimport { SearchIcon } from '@/lib/creance/icons';\nimport { Highlight } from '@/components/creance/combobox/Combobox';\nimport { defaultFilter, useListbox } from '@/components/creance/combobox/useListbox';\nimport '@/components/creance/combobox/Combobox.css';\nimport '@/components/creance/dialog/Dialog.css';\nimport '@/components/creance/menu/Menu.css';\nimport './CommandMenu.css';\n\nexport interface Command {\n  id: string;\n  label: string;\n  /** Heading the command is listed under. Groups keep the order they first appear in. */\n  group?: string;\n  /** Other words that should find it. */\n  keywords?: string[];\n  icon?: ReactNode;\n  /** Shown at the end of the row: a status, a date. */\n  meta?: ReactNode;\n  /** Display only. */\n  shortcut?: ReactNode;\n  tone?: 'default' | 'danger';\n  disabled?: boolean;\n  onSelect: () => void;\n}\n\nexport interface CommandMenuProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  commands: Command[];\n  placeholder?: string;\n  emptyMessage?: ReactNode;\n  /** Toggle with ⌘K (Ctrl+K elsewhere) from anywhere on the page. Default true. */\n  hotkey?: boolean;\n  /** Accessible name of the dialog. Default \"Command menu\". */\n  label?: string;\n  className?: string;\n}\n\nconst isMac = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);\n\n/** Search everything and act from the keyboard: a dialog with a filtered, grouped list of commands. */\nexport function CommandMenu({\n  open,\n  onOpenChange,\n  commands,\n  placeholder = 'Type a command or search',\n  emptyMessage = 'No results',\n  hotkey = true,\n  label = 'Command menu',\n  className,\n}: CommandMenuProps) {\n  const [query, setQuery] = useState('');\n\n  useEffect(() => {\n    if (!hotkey) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key.toLowerCase() === 'k' && (isMac() ? e.metaKey : e.ctrlKey)) {\n        e.preventDefault();\n        onOpenChange(!open);\n      }\n    };\n    window.addEventListener('keydown', onKey);\n    return () => window.removeEventListener('keydown', onKey);\n  }, [hotkey, open, onOpenChange]);\n\n  const items = useMemo(() => {\n    const q = query.trim();\n    const hits = q ? commands.filter((c) => [c.label, ...(c.keywords ?? [])].some((w) => defaultFilter(w, q))) : commands;\n    // Flatten in group order so arrow keys follow what is on screen.\n    const order = [...new Set(hits.map((c) => c.group ?? ''))];\n    return order.flatMap((g) => hits.filter((c) => (c.group ?? '') === g)).map((c) => ({ key: c.id, disabled: c.disabled, command: c }));\n  }, [commands, query]);\n\n  const close = () => {\n    onOpenChange(false);\n    setQuery('');\n  };\n  const run = (item: (typeof items)[number]) => {\n    close();\n    item.command.onSelect();\n  };\n  const list = useListbox(items, open, run);\n\n  return (\n    <RadixDialog.Root open={open} onOpenChange={(o) => (o ? onOpenChange(true) : close())}>\n      <RadixDialog.Portal>\n        <RadixDialog.Overlay className=\"cr-dialog-overlay cr-command-overlay\" />\n        <RadixDialog.Content className={cx('cr-command cr-glass', className)} aria-label={label} aria-describedby={undefined}>\n          <RadixDialog.Title className=\"cr-visually-hidden\">{label}</RadixDialog.Title>\n          <div className=\"cr-command__search\">\n            <SearchIcon />\n            <input\n              role=\"combobox\"\n              aria-expanded=\"true\"\n              aria-controls={list.listId}\n              aria-autocomplete=\"list\"\n              aria-activedescendant={list.activeDescendant}\n              aria-label={placeholder}\n              className=\"cr-command__input\"\n              placeholder={placeholder}\n              value={query}\n              autoComplete=\"off\"\n              onChange={(e) => setQuery(e.currentTarget.value)}\n              onKeyDown={(e) => list.onKeyDown(e, () => {})}\n            />\n            <Kbd>Esc</Kbd>\n          </div>\n          {/* Focus stays in the input; the list and its options are driven by aria-activedescendant. */}\n          {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}\n          <div id={list.listId} role=\"listbox\" aria-label=\"Results\" className=\"cr-command__list\" onMouseDown={(e) => e.preventDefault()}>\n            {items.length === 0 && <p className=\"cr-command__empty\">{emptyMessage}</p>}\n            {items.map((item, i) => {\n              const c = item.command;\n              const heading = c.group && c.group !== items[i - 1]?.command.group ? c.group : null;\n              return (\n                <div key={c.id} role=\"presentation\">\n                  {heading && (\n                    <div role=\"presentation\" className=\"cr-command__group\">\n                      {heading}\n                    </div>\n                  )}\n                  {/* Options are reached from the keyboard through the input (aria-activedescendant). */}\n                  {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */}\n                  <div\n                    id={list.optionId(i)}\n                    role=\"option\"\n                    aria-selected={i === list.active}\n                    aria-disabled={c.disabled || undefined}\n                    className=\"cr-combobox__option\"\n                    data-active={i === list.active || undefined}\n                    data-tone={c.tone}\n                    onMouseMove={() => !c.disabled && list.setActive(i)}\n                    onClick={() => !c.disabled && run(item)}\n                  >\n                    {c.icon}\n                    <Highlight label={c.label} query={query} />\n                    {c.meta && <span className=\"cr-combobox__meta\">{c.meta}</span>}\n                    {c.shortcut && <span className=\"cr-command__shortcut\">{c.shortcut}</span>}\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n          <div className=\"cr-command__footer\" aria-hidden=\"true\">\n            <span>\n              <Kbd>↑</Kbd> <Kbd>↓</Kbd> move\n            </span>\n            <span>\n              <Kbd>↵</Kbd> open\n            </span>\n            <span>\n              <Kbd>Esc</Kbd> close\n            </span>\n          </div>\n        </RadixDialog.Content>\n      </RadixDialog.Portal>\n    </RadixDialog.Root>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/command-menu/CommandMenu.tsx"
    }
  ],
  "categories": [
    "molecule",
    "navigation"
  ],
  "type": "registry:ui"
}