{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command",
  "type": "registry:ui",
  "title": "Command",
  "description": "An accessible React command menu for Next.js and Tailwind CSS: a ⌘K palette that filters and ranks as you type, with groups, shortcuts and a gliding highlight.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "lucide-react@^1.52.0",
    "motion@^14.0.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json",
    "https://ui.fewtone.com/r/dialog.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/command.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { blur, duration, ease, spring } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { SearchIcon } from \"lucide-react\";\nimport {\n  type AnimationPlaybackControls,\n  animate,\n  type HTMLMotionProps,\n  LayoutGroup,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type ComponentProps,\n  createContext,\n  isValidElement,\n  type KeyboardEvent,\n  type KeyboardEventHandler,\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\n/*\n * Command: a composable command menu, the ⌘K palette, with shadcn's parts and no cmdk package (roadmap,\n * 2026-10-16: \"⌘K search for pages and actions\").\n * - Typing filters the items. Each scores its text and `keywords` against the search: an exact match, then\n *   the start of a word, then anywhere, then the letters in order (\"stg\" finds Settings). While searching,\n *   the best matches rise to the top of their group and groups reorder by their best item (CSS order, so\n *   React never moves a node); a group with no matches hides with its heading. Items are hidden, not unmounted.\n * - The field owns the keyboard (a combobox controlling a listbox, with aria-activedescendant): arrows move,\n *   Home and End jump, Enter runs the highlighted item's onSelect. Focus never leaves the field.\n * - Motion (.agents/rules/motion.md):\n *   - one highlight glides between rows (Select's layoutId pill, scoped per list) and jumps while typing\n *     changes the list under it\n *   - the list's height glides as results come and go: an eased tween, never a spring, clipped only while it\n *     moves, carrying on from where it is when the results change again mid-glide\n *   - the empty state comes through the blur\n *   Nothing animates on first paint. With reduced motion the highlight and the height jump and the empty\n *   state only fades.\n * - CommandDialog puts it in our Dialog, held near the top of the screen so the field stays put while the\n *   list glides, and `shortcut` opens it with ⌘ or Ctrl plus a key.\n * - CommandProvider and useCommandInput are the same machinery without the box, for Combobox.\n */\n\n/** How well an item matches the search: 0 hides it, 1 is an exact match */\nexport type CommandFilter = (text: string, search: string, keywords?: string[]) => number;\n\n/** Lower case, single spaces, trimmed */\nfunction normalize(text: string) {\n  return text.toLowerCase().replace(/\\s+/g, \" \").trim();\n}\n\n/** A word starts here: the first letter, or one after a space or punctuation (\"next.js\", \"sign-in\") */\nfunction startsWord(text: string, at: number) {\n  return at === 0 || /[\\s\\-_/.:,()]/.test(text[at - 1] ?? \"\");\n}\n\n/** Whether a word in the text starts with the term */\nfunction hasWordStarting(text: string, term: string) {\n  for (let at = text.indexOf(term); at !== -1; at = text.indexOf(term, at + 1)) {\n    if (startsWord(text, at)) return true;\n  }\n  return false;\n}\n\n/** The letters in order, the first one starting a word. Tighter runs score higher, from 0.1 to 0.3. */\nfunction inOrder(text: string, letters: string) {\n  const [head = \"\", ...rest] = letters;\n  let best = 0;\n  for (let start = text.indexOf(head); start !== -1; start = text.indexOf(head, start + 1)) {\n    if (!startsWord(text, start)) continue;\n    let at = start;\n    for (const letter of rest) {\n      at = text.indexOf(letter, at + 1);\n      if (at === -1) break;\n    }\n    // A later start has fewer letters after it, so it can't match either\n    if (at === -1) break;\n    best = Math.max(best, 0.1 + 0.2 * (letters.length / (at - start + 1)));\n  }\n  return best;\n}\n\n/**\n * The default ranking. In order: an exact match, the text starting with the search, a later word starting\n * with it, the search anywhere in the text, the same two for keywords, every word of the search starting a\n * word, and last the search's letters in order.\n */\nfunction commandFilter(text: string, search: string, keywords: string[] = []) {\n  const query = normalize(search);\n  if (!query) return 1;\n  const value = normalize(text);\n  if (value === query) return 1;\n  if (value.startsWith(query)) return 0.9;\n  if (hasWordStarting(value, query)) return 0.8;\n  if (value.includes(query)) return 0.7;\n  const words = keywords.map(normalize);\n  if (words.some((word) => hasWordStarting(word, query))) return 0.6;\n  if (words.some((word) => word.includes(query))) return 0.5;\n  const terms = query.split(\" \");\n  const all = [value, ...words].join(\" \");\n  if (terms.length > 1 && terms.every((term) => hasWordStarting(all, term))) return 0.4;\n  const letters = query.replaceAll(\" \", \"\");\n  return letters.length > 1 ? inOrder(value, letters) : 0;\n}\n\n/** A score as a whole number: what the item's CSS order and the keyboard both sort by */\nconst rankOf = (score: number) => Math.round(score * 100);\n\nconst ITEM = \"[data-slot=command-item]\";\nconst GROUP = \"[data-slot=command-group]\";\n/** The list's padding: rows scrolled into view keep this much room from its edges */\nconst EDGE = 4;\n\ntype GroupState = { hidden: boolean; order?: number };\n\n/**\n * What the list shows right now, read from the DOM (as Select does), so any markup can sit between the\n * parts: the shown items that can be picked, in the order they appear; each group's state; and how many\n * items are shown at all, disabled ones included.\n */\nfunction readList(list: HTMLElement, ranking: boolean) {\n  const shown = Array.from(list.querySelectorAll<HTMLElement>(ITEM)).filter((item) => !item.hidden);\n  const scoreOf = (item: HTMLElement) => rankOf(Number(item.dataset.score) || 0);\n  const best = new Map<HTMLElement, number>();\n  for (const group of list.querySelectorAll<HTMLElement>(GROUP)) best.set(group, -1);\n  for (const item of shown) {\n    const group = item.closest<HTMLElement>(GROUP);\n    if (group) best.set(group, Math.max(best.get(group) ?? -1, scoreOf(item)));\n  }\n  const groups: Record<string, GroupState> = {};\n  for (const [group, score] of best) {\n    const id = group.dataset.groupId;\n    if (id) groups[id] = { hidden: score < 0, order: ranking && score >= 0 ? -score : undefined };\n  }\n  // The same order CSS shows: groups and loose items by their best score, then where they are; the items\n  // inside a group the same way\n  const first = new Map<HTMLElement, number>();\n  const rows = shown.map((item, index) => {\n    const group = item.closest<HTMLElement>(GROUP);\n    const top = group ?? item;\n    if (!first.has(top)) first.set(top, index);\n    return { item, index, top, own: scoreOf(item), outer: group ? (best.get(group) ?? 0) : scoreOf(item) };\n  });\n  if (ranking) {\n    rows.sort(\n      (a, b) =>\n        b.outer - a.outer ||\n        (first.get(a.top) ?? 0) - (first.get(b.top) ?? 0) ||\n        b.own - a.own ||\n        a.index - b.index,\n    );\n  }\n  return {\n    count: shown.length,\n    groups,\n    items: rows.map((row) => row.item).filter((item) => item.dataset.disabled !== \"true\"),\n  };\n}\n\nfunction sameGroups(a: Record<string, GroupState>, b: Record<string, GroupState>) {\n  const keys = Object.keys(a);\n  return (\n    keys.length === Object.keys(b).length &&\n    keys.every((key) => a[key]?.hidden === b[key]?.hidden && a[key]?.order === b[key]?.order)\n  );\n}\n\n/** Scrolls the list just enough to show the row, and its group's heading when it's the group's first row */\nfunction reveal(list: HTMLElement, item: HTMLElement, previous?: HTMLElement) {\n  const group = item.closest<HTMLElement>(GROUP);\n  const opensGroup = group && previous?.closest(GROUP) !== group;\n  const top = (opensGroup ? group.offsetTop : item.offsetTop) - EDGE;\n  const bottom = item.offsetTop + item.offsetHeight + EDGE;\n  if (top < list.scrollTop) list.scrollTop = top;\n  else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;\n}\n\n/** The plain text of an item's content, for matching the search. A shortcut isn't part of its name. */\nfunction textOf(node: ReactNode): string {\n  if (node == null || typeof node === \"boolean\") return \"\";\n  if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n  if (Array.isArray(node)) return node.map(textOf).join(\"\");\n  if (isValidElement<{ children?: ReactNode }>(node)) {\n    return node.type === CommandShortcut ? \"\" : textOf(node.props.children);\n  }\n  return \"\";\n}\n\n/** Who moved the highlight: the keyboard and the pointer glide it; a list that changed under it jumps it */\ntype Highlight = \"keyboard\" | \"pointer\" | \"list\";\n\ntype CommandContextValue = {\n  search: string;\n  setSearch: (search: string) => void;\n  /** The normalised search the items match against; empty when not filtering */\n  query: string;\n  filter: CommandFilter;\n  label?: string;\n  /** The highlighted item's value */\n  active: string | null;\n  activeId?: string;\n  highlight: (value: string | null, how: Highlight) => void;\n  /** The highlight jumps instead of gliding (typing changed the list) */\n  instant: boolean;\n  reduce: boolean;\n  empty: boolean;\n  groups: Record<string, GroupState>;\n  listId: string;\n  list: HTMLElement | null;\n  setList: (node: HTMLElement | null) => void;\n  register: (id: string, select: RefObject<() => void>) => () => void;\n  /** What the list's status line says: the empty state, for screen readers */\n  announcement: string;\n  announce: (message: string) => void;\n  onKeyDown: (event: KeyboardEvent<HTMLElement>, homeEnd: boolean) => void;\n};\n\nconst CommandContext = createContext<CommandContextValue | null>(null);\n\nfunction useCommand(part: string) {\n  const command = useContext(CommandContext);\n  if (!command) throw new Error(`${part} must be used inside Command`);\n  return command;\n}\n\nexport type CommandProviderProps = {\n  children?: ReactNode;\n  /** The highlighted item's value, when you control it */\n  value?: string;\n  /** The item highlighted at first; otherwise the first item */\n  defaultValue?: string;\n  /** Called when the highlight moves to another item */\n  onValueChange?: (value: string) => void;\n  /** The search, when you control it here rather than on CommandInput */\n  search?: string;\n  /** Called with the search as it changes */\n  onSearchChange?: (search: string) => void;\n  /** Scores an item against the search; 0 hides it */\n  filter?: CommandFilter;\n  /** Set false to filter yourself, for example results from a server */\n  shouldFilter?: boolean;\n  /** ArrowDown on the last item goes back to the first, and ArrowUp the other way */\n  loop?: boolean;\n  /** The search field's accessible name */\n  label?: string;\n};\n\n/** Command's state, without the box: the search, the highlight, filtering and the keyboard */\nfunction CommandProvider({\n  children,\n  value,\n  defaultValue,\n  onValueChange,\n  search: searchProp,\n  onSearchChange,\n  filter = commandFilter,\n  shouldFilter = true,\n  loop = false,\n  label,\n}: CommandProviderProps) {\n  const [ownSearch, setOwnSearch] = useState(\"\");\n  const [ownActive, setOwnActive] = useState<string | null>(defaultValue ?? null);\n  const [instant, setInstant] = useState(false);\n  const [list, setList] = useState<HTMLElement | null>(null);\n  const [view, setView] = useState<{ empty: boolean; activeId?: string; groups: Record<string, GroupState> }>(\n    {\n      empty: false,\n      groups: {},\n    },\n  );\n  const [announcement, announce] = useState(\"\");\n  const [, setVersion] = useState(0);\n  const handlers = useRef(new Map<string, RefObject<() => void>>());\n  const callbacks = useRef({ onValueChange, onSearchChange });\n  const listId = useId();\n  const reduce = useReducedMotion() ?? false;\n  const search = searchProp ?? ownSearch;\n  const query = shouldFilter ? normalize(search) : \"\";\n  const active = value !== undefined ? value || null : ownActive;\n  const lastQuery = useRef(query);\n  const lastList = useRef<HTMLElement | null>(null);\n\n  // When the search changes, the highlight jumps in the same render the rows move (from typing, or a\n  // controlled search)\n  const [seenQuery, setSeenQuery] = useState(query);\n  if (seenQuery !== query) {\n    setSeenQuery(query);\n    setInstant(true);\n  }\n\n  useLayoutEffect(() => {\n    callbacks.current = { onValueChange, onSearchChange };\n  });\n\n  const setSearch = useCallback((next: string) => {\n    setOwnSearch(next);\n    callbacks.current.onSearchChange?.(next);\n  }, []);\n\n  const highlight = useCallback((next: string | null, how: Highlight) => {\n    setInstant(how === \"list\");\n    setOwnActive(next);\n    if (next !== null) callbacks.current.onValueChange?.(next);\n  }, []);\n\n  // Items register their Enter handler; a new or departing item has the list read again\n  const register = useCallback((id: string, select: RefObject<() => void>) => {\n    handlers.current.set(id, select);\n    setVersion((version) => version + 1);\n    return () => {\n      handlers.current.delete(id);\n      setVersion((version) => version + 1);\n    };\n  }, []);\n\n  // After every render, read what the list shows: which groups hide and how they're ordered, whether it's\n  // empty, and which item is highlighted. A new search highlights its best match, from the top; so does a\n  // list that lost the highlighted item. Runs before paint, so nothing flickers.\n  useLayoutEffect(() => {\n    if (!list) {\n      lastList.current = null;\n      return;\n    }\n    const { items, groups, count } = readList(list, query !== \"\");\n    const searched = lastQuery.current !== query;\n    const mounted = lastList.current !== list;\n    lastQuery.current = query;\n    lastList.current = list;\n    const current = items.find((item) => item.dataset.value === active);\n    const next = searched || !current ? items[0] : current;\n    const nextValue = next?.dataset.value ?? null;\n    if (searched) list.scrollTop = 0;\n    // A list opening on a highlighted item (Combobox's chosen option) shows it in the middle\n    else if (mounted && next) list.scrollTop = next.offsetTop - (list.clientHeight - next.offsetHeight) / 2;\n    if (nextValue !== active) highlight(nextValue, \"list\");\n    const empty = count === 0;\n    setView((previous) =>\n      previous.empty === empty && previous.activeId === next?.id && sameGroups(previous.groups, groups)\n        ? previous\n        : { empty, activeId: next?.id, groups },\n    );\n  });\n\n  function onKeyDown(event: KeyboardEvent<HTMLElement>, homeEnd: boolean) {\n    if (!list || event.nativeEvent.isComposing) return;\n    const { items } = readList(list, query !== \"\");\n    const index = items.findIndex((item) => item.dataset.value === active);\n    const last = items.length - 1;\n    let next: number | undefined;\n    switch (event.key) {\n      case \"ArrowDown\":\n        next = index < 0 ? 0 : index < last ? index + 1 : loop ? 0 : last;\n        break;\n      case \"ArrowUp\":\n        next = index < 0 ? last : index > 0 ? index - 1 : loop ? last : 0;\n        break;\n      case \"Home\":\n        if (homeEnd) next = 0;\n        break;\n      case \"End\":\n        if (homeEnd) next = last;\n        break;\n      case \"Enter\": {\n        const item = items[index];\n        if (!item) return;\n        event.preventDefault();\n        handlers.current.get(item.id)?.current();\n        return;\n      }\n    }\n    if (next === undefined) return;\n    event.preventDefault();\n    const item = items[next];\n    if (!item || item.dataset.value === active) return;\n    // Scroll first, so the highlight's glide is measured where the rows now are\n    reveal(list, item, items[next - 1]);\n    highlight(item.dataset.value ?? null, \"keyboard\");\n  }\n\n  return (\n    <CommandContext.Provider\n      value={{\n        search,\n        setSearch,\n        query,\n        filter,\n        label,\n        active,\n        activeId: view.activeId,\n        highlight,\n        instant,\n        reduce,\n        empty: view.empty,\n        groups: view.groups,\n        listId,\n        list,\n        setList,\n        register,\n        announcement,\n        announce,\n        onKeyDown,\n      }}\n    >\n      {children}\n    </CommandContext.Provider>\n  );\n}\n\nexport type CommandProps = Omit<ComponentProps<\"div\">, \"defaultValue\"> &\n  Omit<CommandProviderProps, \"children\">;\n\nfunction Command({\n  className,\n  value,\n  defaultValue,\n  onValueChange,\n  search,\n  onSearchChange,\n  filter,\n  shouldFilter,\n  loop,\n  label,\n  ...props\n}: CommandProps) {\n  return (\n    <CommandProvider\n      value={value}\n      defaultValue={defaultValue}\n      onValueChange={onValueChange}\n      search={search}\n      onSearchChange={onSearchChange}\n      filter={filter}\n      shouldFilter={shouldFilter}\n      loop={loop}\n      label={label}\n    >\n      <div\n        data-slot=\"command\"\n        className={cn(\n          \"flex w-full flex-col overflow-hidden rounded-lg bg-popover text-popover-foreground\",\n          className,\n        )}\n        {...props}\n      />\n    </CommandProvider>\n  );\n}\n\n/**\n * The combobox wiring for a search field inside Command: its role, the list it controls, the highlighted\n * row (aria-activedescendant) and the keyboard. CommandInput uses it; Combobox puts it on its own field.\n * Your onKeyDown runs first; call event.preventDefault() to keep a key for yourself.\n */\nfunction useCommandInput({\n  onKeyDown,\n  homeEnd = true,\n}: {\n  onKeyDown?: KeyboardEventHandler<HTMLInputElement>;\n  /** Home and End jump to the first and last items; false leaves them to the text */\n  homeEnd?: boolean;\n} = {}) {\n  const command = useCommand(\"useCommandInput\");\n  return {\n    role: \"combobox\",\n    \"aria-expanded\": true,\n    \"aria-autocomplete\": \"list\",\n    \"aria-controls\": command.listId,\n    \"aria-activedescendant\": command.list ? command.activeId : undefined,\n    onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {\n      onKeyDown?.(event);\n      if (!event.defaultPrevented) command.onKeyDown(event, homeEnd);\n    },\n  } as const;\n}\n\nexport type CommandInputProps = Omit<ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"type\"> & {\n  /** The search, when you control it */\n  value?: string;\n  /** Called with the search as it changes */\n  onValueChange?: (search: string) => void;\n};\n\nfunction CommandInput({ className, value, onValueChange, onChange, onKeyDown, ...props }: CommandInputProps) {\n  const command = useCommand(\"CommandInput\");\n  const field = useCommandInput({ onKeyDown });\n  const { setSearch } = command;\n\n  // A controlled search follows the prop\n  useLayoutEffect(() => {\n    if (value !== undefined) setSearch(value);\n  }, [value, setSearch]);\n\n  return (\n    <div\n      data-slot=\"command-input-wrapper\"\n      className=\"flex shrink-0 items-center gap-2 border-b border-border px-3\"\n    >\n      <SearchIcon aria-hidden className=\"size-(--size-icon-md) shrink-0 text-muted-foreground\" />\n      <input\n        data-slot=\"command-input\"\n        type=\"text\"\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        spellCheck={false}\n        aria-label={props[\"aria-labelledby\"] ? undefined : (command.label ?? props.placeholder)}\n        {...field}\n        value={value ?? command.search}\n        onChange={(event) => {\n          onChange?.(event);\n          if (value === undefined) setSearch(event.target.value);\n          onValueChange?.(event.target.value);\n        }}\n        className={cn(\n          \"h-(--size-control-lg) w-full min-w-0 bg-transparent text-sm outline-hidden placeholder:text-muted-foreground\",\n          \"disabled:cursor-not-allowed disabled:opacity-(--opacity-disabled)\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\n/**\n * Glides `box` to the height its content needs whenever the content (`content`) changes size: the height\n * pattern, an eased tween (a spring's overshoot would bounce what's below) on the box's own min and max\n * height, clipped only while it moves. A change mid-glide carries on from where it is. Instant with reduced\n * motion, and nothing moves on first paint. Inside our Popover it stands aside, because the popover already\n * glides its own height. Shared with Combobox's field. Returns the height it's heading for while it moves.\n */\nfunction useGlidingHeight(box: RefObject<HTMLElement | null>, content: RefObject<HTMLElement | null>) {\n  const reduce = useReducedMotion() ?? false;\n  const heading = useRef<number | null>(null);\n\n  useLayoutEffect(() => {\n    const outer = box.current;\n    const inner = content.current;\n    if (!outer || !inner || reduce || outer.closest(\"[data-slot=popover-content]\")) return;\n    // Layout pixels (offsets), so a dialog or popover scaling in doesn't count\n    const natural = () => {\n      const style = getComputedStyle(outer);\n      const max = Number.parseFloat(style.maxHeight);\n      const min = Number.parseFloat(style.minHeight) || 0;\n      const height = inner.offsetHeight + outer.offsetHeight - outer.clientHeight;\n      return Math.max(min, Number.isNaN(max) ? height : Math.min(height, max));\n    };\n    let to = outer.offsetHeight;\n    let glide: AnimationPlaybackControls | undefined;\n    const release = () => {\n      glide = undefined;\n      heading.current = null;\n      outer.style.height = \"\";\n      outer.style.overflow = \"\";\n    };\n    // ResizeObserver runs after layout and before paint, so the new size is never shown before the glide\n    const observer = new ResizeObserver(() => {\n      const next = natural();\n      if (next === to) return;\n      const from = glide ? outer.offsetHeight : to;\n      glide?.stop();\n      to = next;\n      heading.current = next;\n      outer.style.height = `${from}px`;\n      outer.style.overflow = \"hidden\";\n      glide = animate(\n        outer,\n        { height: [from, next] },\n        { duration: duration.slow, ease: ease.out, onComplete: release },\n      );\n    });\n    observer.observe(inner);\n    return () => {\n      observer.disconnect();\n      glide?.stop();\n      release();\n    };\n  }, [box, content, reduce]);\n\n  return heading;\n}\n\n/** The soft edge shown where the list can scroll further, as on Select */\nconst fade =\n  \"pointer-events-none absolute inset-x-0 z-10 h-6 from-popover to-transparent transition-opacity duration-(--duration-fast)\";\n\nexport type CommandListProps = ComponentProps<\"div\"> & {\n  /** The list's accessible name */\n  label?: string;\n};\n\nfunction CommandList({ className, children, label = \"Results\", ref, onScroll, ...props }: CommandListProps) {\n  const command = useCommand(\"CommandList\");\n  const scroller = useRef<HTMLDivElement | null>(null);\n  const sizer = useRef<HTMLDivElement>(null);\n  const layoutGroup = useId();\n  const [edges, setEdges] = useState({ top: false, bottom: false });\n  const heading = useGlidingHeight(scroller, sizer);\n  const { setList } = command;\n\n  // The fades show there's more to scroll to. While the height glides, the bottom is judged by where it's\n  // heading, so no fade flashes on the way.\n  const updateEdges = useCallback(() => {\n    const node = scroller.current;\n    const inner = sizer.current;\n    if (!node || !inner) return;\n    const height = heading.current ?? node.clientHeight;\n    const top = node.scrollTop > 1;\n    const bottom = node.scrollTop + height < inner.offsetHeight - 1;\n    setEdges((current) => (current.top === top && current.bottom === bottom ? current : { top, bottom }));\n  }, [heading]);\n\n  useLayoutEffect(() => {\n    const node = scroller.current;\n    const inner = sizer.current;\n    if (!node || !inner) return;\n    updateEdges();\n    const observer = new ResizeObserver(updateEdges);\n    observer.observe(node);\n    observer.observe(inner);\n    return () => observer.disconnect();\n  }, [updateEdges]);\n\n  const setRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      scroller.current = node;\n      setList(node);\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref, setList],\n  );\n\n  return (\n    // Pressing anywhere in the list keeps focus in the search field (rows, the scrollbar, the empty state)\n    // biome-ignore lint/a11y/noStaticElementInteractions: not an action, it only keeps focus where it is\n    <div\n      data-slot=\"command-list-frame\"\n      className=\"relative isolate flex min-h-0 flex-col\"\n      onMouseDown={(event) => event.preventDefault()}\n    >\n      <div\n        aria-hidden\n        className={cn(fade, \"top-0 bg-linear-to-b\", edges.top ? \"opacity-100\" : \"opacity-0\")}\n      />\n      {/* layoutScroll keeps the highlight in place while the list scrolls. Out of the tab order: the\n          field moves through the rows. */}\n      <motion.div\n        ref={setRef}\n        id={command.listId}\n        role=\"listbox\"\n        aria-label={label}\n        tabIndex={-1}\n        layoutScroll\n        data-slot=\"command-list\"\n        onScroll={(event) => {\n          updateEdges();\n          onScroll?.(event);\n        }}\n        className={cn(\n          \"relative max-h-72 min-h-0 overflow-y-auto overscroll-contain outline-hidden\",\n          className,\n        )}\n        {...(props as HTMLMotionProps<\"div\">)}\n      >\n        <div ref={sizer} className=\"flex flex-col p-1\">\n          <LayoutGroup id={layoutGroup}>{children}</LayoutGroup>\n        </div>\n      </motion.div>\n      <div\n        aria-hidden\n        className={cn(fade, \"bottom-0 bg-linear-to-t\", edges.bottom ? \"opacity-100\" : \"opacity-0\")}\n      />\n      <span role=\"status\" className=\"sr-only\">\n        {command.announcement}\n      </span>\n    </div>\n  );\n}\n\n/**\n * Shown when nothing matches. It comes through the blur. Screen readers hear it from the list's status\n * line, so the message itself is hidden from them.\n */\nfunction CommandEmpty({ className, children = \"No results found.\", ref, ...props }: ComponentProps<\"div\">) {\n  const command = useCommand(\"CommandEmpty\");\n  const node = useRef<HTMLDivElement | null>(null);\n  const { announce, empty: shown } = command;\n  const setRef = (element: HTMLDivElement | null) => {\n    node.current = element;\n    if (typeof ref === \"function\") ref(element);\n    else if (ref) ref.current = element;\n  };\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: the message can change with the search\n  useLayoutEffect(() => {\n    announce(shown ? (node.current?.textContent ?? \"\") : \"\");\n  }, [shown, command.search, announce]);\n  useLayoutEffect(() => () => announce(\"\"), [announce]);\n\n  if (!shown) return null;\n  return (\n    <motion.div\n      ref={setRef}\n      data-slot=\"command-empty\"\n      aria-hidden\n      // Only after a search: a list that starts out empty is simply there\n      initial={command.search ? { opacity: 0, filter: command.reduce ? blur.shown : blur.hidden } : false}\n      animate={{ opacity: 1, filter: blur.shown, transitionEnd: { filter: \"none\" } }}\n      transition={{ duration: duration.base, ease: ease.out }}\n      className={cn(\"py-6 text-center text-sm text-muted-foreground\", className)}\n      {...(props as HTMLMotionProps<\"div\">)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type CommandGroupProps = ComponentProps<\"div\"> & {\n  /** The group's heading, which also names it */\n  heading?: ReactNode;\n};\n\nfunction CommandGroup({ className, heading, children, style, ...props }: CommandGroupProps) {\n  const command = useCommand(\"CommandGroup\");\n  const id = useId();\n  const headingId = useId();\n  const state = command.groups[id];\n\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a group of options in a listbox, not form fields, so not a fieldset\n    <div\n      data-slot=\"command-group\"\n      data-group-id={id}\n      role=\"group\"\n      aria-labelledby={heading ? headingId : undefined}\n      // A group with no matches hides, heading included; while searching, the best group comes first\n      hidden={state?.hidden}\n      style={{ order: state?.order, ...style }}\n      className={cn(\"flex flex-col\", className)}\n      {...props}\n    >\n      {heading ? (\n        <div\n          id={headingId}\n          data-slot=\"command-group-heading\"\n          className=\"order-first px-2 py-1.5 text-xs font-medium text-muted-foreground\"\n        >\n          {heading}\n        </div>\n      ) : null}\n      {children}\n    </div>\n  );\n}\n\nexport type CommandItemProps = Omit<ComponentProps<\"div\">, \"onSelect\" | \"value\"> & {\n  /** What onSelect receives and the highlight tracks. Defaults to the item's text; keep it unique. */\n  value?: string;\n  /** More words the search matches, for example \"preferences\" for Settings */\n  keywords?: string[];\n  /** The text the search matches, when the content isn't plain text */\n  textValue?: string;\n  /** Runs on Enter or a click */\n  onSelect?: (value: string) => void;\n  disabled?: boolean;\n  /** Always shown, whatever the search */\n  forceMount?: boolean;\n};\n\nfunction CommandItem({\n  value: valueProp,\n  keywords,\n  textValue,\n  onSelect,\n  disabled = false,\n  forceMount = false,\n  className,\n  children,\n  id,\n  style,\n  onPointerMove,\n  onClick,\n  ...props\n}: CommandItemProps) {\n  const command = useCommand(\"CommandItem\");\n  const fallbackId = useId();\n  const itemId = id ?? fallbackId;\n  const text = textValue ?? textOf(children);\n  const value = (valueProp ?? text).trim();\n  const score = forceMount || !command.query ? 1 : command.filter(text, command.search, keywords);\n  const hidden = score <= 0;\n  const active = !hidden && command.active === value;\n  const select = useRef(() => {});\n  const { register } = command;\n\n  // Enter runs the latest onSelect\n  useLayoutEffect(() => {\n    select.current = () => {\n      if (!disabled) onSelect?.(value);\n    };\n  });\n  useLayoutEffect(() => register(itemId, select), [itemId, register]);\n\n  return (\n    // The field keeps focus and the keyboard (aria-activedescendant points here), so the row needs neither\n    // biome-ignore lint/a11y/useFocusableInteractive: focus stays in the search field\n    // biome-ignore lint/a11y/useKeyWithClickEvents: Enter in the search field runs it\n    <div\n      id={itemId}\n      role=\"option\"\n      aria-selected={active}\n      aria-disabled={disabled || undefined}\n      data-slot=\"command-item\"\n      data-value={value}\n      data-score={score}\n      data-selected={active}\n      data-disabled={disabled}\n      hidden={hidden}\n      // While searching, CSS order puts the best matches first; ties keep their place\n      style={{ order: command.query && !hidden ? -rankOf(score) : undefined, ...style }}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        if (!disabled && !active && !event.defaultPrevented) command.highlight(value, \"pointer\");\n      }}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!disabled && !event.defaultPrevented) onSelect?.(value);\n      }}\n      className={cn(\n        \"relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none\",\n        \"data-[selected=true]:text-accent-foreground\",\n        \"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-(--opacity-disabled)\",\n        \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md) [&_svg:not([class*='text-'])]:text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {/* Rows are not stacking contexts: the highlight joins the list's own layer (it's `isolate`), so it\n          passes behind every row's text, whichever way it glides */}\n      {active ? (\n        <motion.span\n          layoutId=\"command-highlight\"\n          aria-hidden\n          className=\"absolute inset-0 -z-10 rounded-md bg-accent\"\n          transition={command.instant || command.reduce ? { duration: 0 } : spring.snappy}\n        />\n      ) : null}\n      {children}\n    </div>\n  );\n}\n\nfunction CommandSeparator({\n  className,\n  alwaysRender = false,\n  ...props\n}: ComponentProps<\"div\"> & {\n  /** Keep it while searching (it hides by default, since the matches read as one list) */\n  alwaysRender?: boolean;\n}) {\n  const command = useCommand(\"CommandSeparator\");\n  return (\n    <div\n      data-slot=\"command-separator\"\n      aria-hidden\n      hidden={!!command.query && !alwaysRender}\n      className={cn(\"-mx-1 my-1 h-px shrink-0 bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\n/** A keyboard shortcut at the end of an item. The search doesn't match it. */\nfunction CommandShortcut({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"command-shortcut\"\n      className={cn(\"ms-auto inline-flex items-center gap-1 text-xs text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type CommandDialogProps = Omit<ComponentProps<typeof Dialog>, \"children\"> &\n  Omit<CommandProviderProps, \"children\"> & {\n    children?: ReactNode;\n    /** Names the dialog for screen readers (it isn't shown) */\n    title?: string;\n    /** Describes the dialog for screen readers (it isn't shown) */\n    description?: string;\n    /** Classes for the window */\n    className?: string;\n    /** Show a close button in the top corner. Escape and the backdrop close it either way. */\n    showCloseButton?: boolean;\n    /** Opens and closes it with ⌘ (Ctrl on Windows and Linux) plus this key, for example \"k\" */\n    shortcut?: string;\n  };\n\n/** The palette in a Dialog. Put CommandInput and CommandList inside, as in Command. */\nfunction CommandDialog({\n  title = \"Command palette\",\n  description = \"Search for a command to run.\",\n  className,\n  showCloseButton = false,\n  shortcut,\n  open,\n  defaultOpen,\n  onOpenChange,\n  modal,\n  children,\n  ...command\n}: CommandDialogProps) {\n  const [own, setOwn] = useState(defaultOpen ?? false);\n  const current = open ?? own;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      setOwn(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange],\n  );\n\n  useEffect(() => {\n    if (!shortcut) return;\n    const key = shortcut.toLowerCase();\n    function onKey(event: globalThis.KeyboardEvent) {\n      // Another handler already took the key, or it's a different chord\n      if (event.defaultPrevented || event.repeat || event.isComposing) return;\n      if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;\n      // Letters also match by their place on the keyboard, for layouts that type another character\n      if (event.key.toLowerCase() !== key && event.code !== `Key${key.toUpperCase()}`) return;\n      event.preventDefault();\n      setOpen(!current);\n    }\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [shortcut, current, setOpen]);\n\n  return (\n    <Dialog open={current} onOpenChange={setOpen} modal={modal}>\n      <DialogContent\n        showCloseButton={showCloseButton}\n        // Near the top rather than centred, so the field stays put while the list's height glides\n        className={cn(\n          \"gap-0 self-start overflow-hidden bg-popover p-0 text-popover-foreground sm:mt-20\",\n          className,\n        )}\n      >\n        <DialogHeader className=\"sr-only\">\n          <DialogTitle>{title}</DialogTitle>\n          <DialogDescription>{description}</DialogDescription>\n        </DialogHeader>\n        <Command className=\"rounded-none\" {...command}>\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport {\n  Command,\n  CommandDialog,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandProvider,\n  CommandSeparator,\n  CommandShortcut,\n  commandFilter,\n  // Shared with Combobox\n  useCommandInput,\n  useGlidingHeight,\n};\n"
    }
  ],
  "categories": [
    "navigation"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/command"
}
