{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command",
  "type": "registry:component",
  "title": "Command",
  "description": "A launcher: type to find a page, an app or an action, and run it with Enter.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [
    "https://panelui.dev/r/web/spinner.json"
  ],
  "files": [
    {
      "path": "components/panelui/command.tsx",
      "type": "registry:component",
      "target": "@components/panelui/command.tsx",
      "content": "'use client';\n\n/**\n * Command — a launcher: type to find an app, a page or an action, and run it with Enter.\n *\n * ```tsx\n * <Command.Dialog hotkey=\"mod+k\">\n *   <Command>\n *     <Command.Input placeholder=\"Search for pages and actions…\" />\n *     <Command.List>\n *       <Command.Group heading=\"Pages\">\n *         <Command.Item icon={<BookIcon />} kind=\"Page\" onSelect={() => go('/docs')}>\n *           Documentation\n *         </Command.Item>\n *       </Command.Group>\n *     </Command.List>\n *     <Command.Footer>\n *       <Command.FooterAction keys={['↵']}>Open</Command.FooterAction>\n *     </Command.Footer>\n *   </Command>\n * </Command.Dialog>\n * ```\n *\n * ## Typing filters, Enter runs\n *\n * Items are written as children, and each hides itself when it does not\n * match what is typed: every word has to appear in its title, `subtitle`,\n * `kind` or `keywords`, in any order. A group with nothing left in it hides\n * too, and `Command.Empty` shows when nothing at all is left. The first match\n * is highlighted, so typing a few letters and pressing Enter runs it.\n *\n * ## It opens at once\n *\n * A launcher is opened many times a day, mostly from its hotkey, so opened\n * from the keyboard it appears in place, with no animation to wait for.\n * Opened with a pointer, it fades in over 150ms.\n *\n * ## Five layouts\n *\n * `spotlight` is the large launcher: a tall field, roomy rows with app icons\n * and the kind of each at its end, and a bar of actions at the foot.\n * `compact` is a dense palette for an editor, with each item's shortcut at\n * its end. `grid` lays the items out as tiles that the arrow keys move\n * across. `split` keeps a preview of the highlighted item beside the list.\n * `inline` sits in the page as a card rather than over it.\n */\nimport { Autocomplete } from '@base-ui/react/autocomplete';\nimport { Dialog } from '@base-ui/react/dialog';\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactElement,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { Spinner } from '@/components/panelui/spinner';\n\nexport type CommandVariant = 'spotlight' | 'compact' | 'grid' | 'split' | 'inline';\n\nconst commandVariants = tv({\n  slots: {\n    // A size container, so the split layout can leave its preview out when narrow.\n    root: 'group/command @container relative flex min-h-0 w-full flex-col overflow-hidden text-popover-foreground',\n    inputRow: 'flex shrink-0 items-center gap-3 border-b',\n    inputIcon: 'flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-full',\n    input: 'h-full min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground/75',\n    prefix: [\n      'inline-flex h-7 shrink-0 items-center gap-1.5 rounded-lg bg-muted px-2 text-[0.8125rem] font-medium text-foreground',\n      '[&_svg]:size-3.5',\n    ],\n    inputAction: [\n      'inline-flex h-8 shrink-0 cursor-pointer items-center gap-2 rounded-lg ps-2.5 pe-1 text-sm text-muted-foreground outline-none',\n      'transition-[background-color,color,scale] duration-150 hover:bg-accent hover:text-foreground active:scale-[0.97]',\n      'focus-visible:ring-[3px] focus-visible:ring-ring/30',\n    ],\n    body: 'flex min-h-0 flex-1',\n    list: [\n      'min-h-0 flex-1 overflow-y-auto overscroll-contain [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin]',\n    ],\n    group: '[&:not(:has([data-slot=command-item]))]:hidden',\n    groupLabel: 'select-none text-xs font-medium text-muted-foreground',\n    item: [\n      'group/item relative flex cursor-default select-none items-center outline-none',\n      'data-highlighted:bg-accent data-disabled:opacity-45',\n    ],\n    itemIcon: 'flex shrink-0 items-center justify-center [&>svg]:size-full',\n    itemTitle: 'truncate text-foreground',\n    // On a narrow launcher the subtitle gives its room to the title.\n    itemSubtitle: 'hidden truncate text-muted-foreground @[30rem]:block',\n    itemEnd: 'ms-auto flex shrink-0 items-center gap-2 ps-3 text-muted-foreground [&>span>svg]:size-4',\n    badge:\n      'inline-flex h-5 items-center rounded-md bg-info/12 px-1.5 text-[0.6875rem] font-medium text-info',\n    keys: 'flex items-center gap-1',\n    key: [\n      'inline-flex h-5 min-w-5 items-center justify-center rounded-md border bg-muted/50 px-1 font-sans text-[0.6875rem] font-medium leading-none text-muted-foreground',\n    ],\n    empty: 'flex flex-col items-center justify-center gap-1 px-4 py-10 text-center text-sm text-muted-foreground',\n    loading: 'flex items-center justify-center gap-2 px-4 py-8 text-sm text-muted-foreground',\n    separator: 'mx-3 my-1.5 h-px shrink-0 bg-border',\n    footer: 'flex shrink-0 items-center gap-1 border-t',\n    footerLead: 'me-auto flex min-w-0 items-center gap-2 text-[0.8125rem] text-muted-foreground [&_svg]:size-4',\n    // Where the actions sit: inline in most layouts, a floating pill in `spotlight`.\n    footerBar: 'flex items-center gap-1',\n    footerButton: [\n      'inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-lg text-muted-foreground outline-none',\n      'transition-[background-color,color,scale] duration-150 hover:bg-accent hover:text-foreground active:scale-[0.96]',\n      'focus-visible:ring-[3px] focus-visible:ring-ring/30 data-popup-open:bg-accent [&_svg]:size-4',\n    ],\n    footerAction: [\n      'inline-flex h-7 shrink-0 cursor-pointer items-center gap-2 rounded-lg ps-2.5 pe-1 text-[0.8125rem] font-medium text-foreground outline-none',\n      'transition-[background-color,scale] duration-150 hover:bg-accent active:scale-[0.97] focus-visible:ring-[3px] focus-visible:ring-ring/30',\n      'data-popup-open:bg-accent',\n    ],\n    footerDivider: 'mx-1 h-4 w-px bg-border',\n    pane: 'hidden w-[45%] shrink-0 flex-col overflow-y-auto border-s bg-muted/30 @[36rem]:flex',\n    gridRow: 'grid gap-1',\n    backdrop: [\n      'fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px]',\n      'transition-opacity duration-150 ease-out data-starting-style:opacity-0 data-ending-style:opacity-0',\n      'in-data-instant:transition-none motion-reduce:transition-none',\n    ],\n    viewport: 'fixed inset-0 z-50 flex items-start justify-center px-3 pt-[12vh]',\n    popup: [\n      '@container flex max-h-[min(34rem,78vh)] w-full flex-col overflow-hidden rounded-2xl border bg-popover shadow-2xl shadow-black/25 outline-none',\n      'data-[variant=spotlight]:rounded-[1.375rem]',\n      'transition-[opacity,scale] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]',\n      'data-starting-style:scale-[0.98] data-starting-style:opacity-0 data-ending-style:scale-[0.98] data-ending-style:opacity-0',\n      // Opened from the keyboard: no animation at all.\n      'data-instant:transition-none motion-reduce:transition-none',\n    ],\n  },\n  variants: {\n    variant: {\n      spotlight: {\n        root: 'rounded-[1.375rem]',\n        // One sheet: no rule between the field and the list.\n        inputRow: 'h-[4.25rem] border-b-0 px-6',\n        inputIcon: 'size-6',\n        input: 'text-xl placeholder:text-muted-foreground/60',\n        list: [\n          'scroll-py-2 px-2 pb-2',\n          // With a footer, the last rows run on under its floating pill and fade out.\n          'group-has-[[data-slot=command-footer]]/command:pb-20',\n          'group-has-[[data-slot=command-footer]]/command:[mask-image:linear-gradient(to_bottom,black_calc(100%-6rem),transparent_calc(100%-1.5rem))]',\n        ],\n        group: 'not-first:mt-1',\n        groupLabel: 'px-3 pb-1 pt-2 text-[0.8125rem]',\n        item: 'h-[2.375rem] gap-3 rounded-[0.625rem] px-3 text-[0.9375rem]',\n        itemIcon: 'size-[1.375rem] [&>*]:rounded-[0.4rem]',\n        itemSubtitle: 'text-[0.9375rem]',\n        itemEnd: 'text-[0.9375rem] @max-[30rem]:[&>[data-slot=command-keys]]:hidden',\n        key: 'h-6 min-w-6 rounded-[0.4rem] border-foreground/15 bg-transparent text-[0.75rem] text-foreground/75',\n        inputAction: 'h-9 gap-2.5 ps-3 pe-1.5 text-[0.9375rem] hover:bg-accent/70',\n        footer: 'pointer-events-none absolute inset-x-3 bottom-3 border-t-0',\n        footerLead: 'pointer-events-auto',\n        footerBar: 'pointer-events-auto h-11 gap-0.5 rounded-full border bg-popover px-1.5 shadow-lg shadow-black/20',\n        // On a narrow launcher the pill keeps the labels and drops the key caps, so it fits.\n        footerAction: 'h-8 rounded-full ps-3.5 pe-1.5 text-[0.9375rem] @max-[30rem]:px-3 @max-[30rem]:[&>[data-slot=command-keys]]:hidden',\n        footerButton: 'pointer-events-auto size-11 rounded-full border bg-popover shadow-lg shadow-black/20 [&_svg]:size-[1.125rem]',\n        footerDivider: 'mx-1.5 h-5',\n      },\n      compact: {\n        inputRow: 'h-11 px-3.5',\n        inputIcon: 'size-4',\n        input: 'text-sm',\n        list: 'scroll-py-1 p-1',\n        group: 'not-first:mt-1',\n        groupLabel: 'px-2.5 pb-1 pt-2 text-[0.6875rem]',\n        item: 'h-8 gap-2.5 rounded-lg px-2.5 text-[0.8125rem]',\n        itemIcon: 'size-4 text-muted-foreground group-data-highlighted/item:text-foreground',\n        itemSubtitle: 'text-xs',\n        itemEnd: 'text-xs',\n        footer: 'h-9 px-2 text-xs',\n      },\n      grid: {\n        inputRow: 'h-14 px-4',\n        inputIcon: 'size-5',\n        input: 'text-base',\n        list: 'scroll-py-2 p-2',\n        group: 'not-first:mt-3',\n        groupLabel: 'px-2 pb-2 pt-1',\n        item: 'h-24 flex-col justify-center gap-2 rounded-xl px-1.5 text-center text-xs',\n        itemIcon: 'size-9 [&>*]:rounded-lg',\n        itemTitle: 'w-full',\n        itemSubtitle: 'hidden',\n        itemEnd: 'hidden',\n        footer: 'h-11 bg-muted/30 px-3',\n      },\n      split: {\n        inputRow: 'h-14 px-4',\n        inputIcon: 'size-5',\n        input: 'text-base',\n        list: 'scroll-py-1.5 p-1.5',\n        group: 'not-first:mt-2',\n        groupLabel: 'px-2.5 pb-1 pt-2',\n        item: 'h-10 gap-3 rounded-lg px-2.5 text-sm data-highlighted:bg-accent',\n        itemIcon: 'size-5 [&>*]:rounded-md',\n        itemSubtitle: 'hidden',\n        itemEnd: 'text-xs',\n        footer: 'h-11 bg-muted/30 px-3',\n      },\n      inline: {\n        root: 'rounded-2xl border bg-card shadow-xs',\n        inputRow: 'h-12 px-4',\n        inputIcon: 'size-4',\n        input: 'text-sm',\n        list: 'max-h-80 scroll-py-1.5 p-1.5',\n        group: 'not-first:mt-1.5',\n        groupLabel: 'px-2.5 pb-1 pt-2',\n        item: 'min-h-12 gap-3 rounded-xl px-2.5 py-2 text-sm',\n        itemIcon: 'size-8 rounded-lg border bg-background [&_svg]:size-4',\n        itemSubtitle: 'text-xs',\n        itemEnd: 'text-xs',\n        footer: 'h-10 px-3 text-xs',\n      },\n    },\n  },\n  defaultVariants: { variant: 'spotlight' },\n});\n\nconst DIALOG_WIDTH: Record<CommandVariant, string> = {\n  spotlight: 'max-w-[47rem]',\n  compact: 'max-w-[34rem]',\n  grid: 'max-w-[40rem]',\n  split: 'max-w-[50rem]',\n  inline: 'max-w-[40rem]',\n};\n\n/* -------------------------------------------------------------------------- */\n/* Matching                                                                   */\n/* -------------------------------------------------------------------------- */\n\nfunction textOf(node: ReactNode): string {\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)) return textOf(node.props.children);\n  return '';\n}\n\n/** Every word typed has to appear somewhere in what the item says about itself. */\nfunction matches(query: string, haystack: string) {\n  const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n  if (words.length === 0) return true;\n  const text = haystack.toLowerCase();\n  return words.every((word) => text.includes(word));\n}\n\nfunction haystackOf(props: CommandItemProps) {\n  return [props.textValue ?? textOf(props.children), textOf(props.subtitle), textOf(props.kind), ...(props.keywords ?? [])].join(' ');\n}\n\n/* -------------------------------------------------------------------------- */\n/* Root                                                                       */\n/* -------------------------------------------------------------------------- */\n\ninterface Registered {\n  title: ReactNode;\n}\n\ninterface CommandContextValue {\n  variant: CommandVariant;\n  slots: ReturnType<typeof commandVariants>;\n  search: string;\n  filtering: boolean;\n  columns: number;\n  highlighted: string | undefined;\n  register: (value: string, data: Registered) => () => void;\n  count: number;\n  close: () => 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\n/** Set by `Command.Dialog`, so a chosen item can close it. */\nconst DialogContext = createContext<{ close: () => void; variant: CommandVariant | null; setVariant: (variant: CommandVariant) => void } | null>(\n  null\n);\n\nexport interface CommandProps extends Omit<ComponentProps<'div'>, 'onChange'> {\n  /** The layout: `spotlight`, `compact`, `grid`, `split` or `inline`. Default `spotlight`. */\n  variant?: CommandVariant;\n  /** What is typed in the field, when the caller holds it. */\n  search?: string;\n  /** Called with the text in the field as it changes. */\n  onSearchChange?: (search: string) => void;\n  /** Whether items hide when they do not match. Turn it off for results already filtered elsewhere, such as by a server. Default `true`. */\n  filter?: boolean;\n  /** Tiles to a row, for the `grid` layout. Default `5`. */\n  columns?: number;\n  /** What a screen reader calls the list. Default `Commands`. */\n  label?: string;\n}\n\n/** The launcher: a field, the list it filters and an optional footer. Put it in a `Command.Dialog`, or in the page. */\nfunction CommandRoot({\n  variant = 'spotlight',\n  search: searchProp,\n  onSearchChange,\n  filter = true,\n  columns = 5,\n  label = 'Commands',\n  className,\n  children,\n  ...props\n}: CommandProps) {\n  const dialog = useContext(DialogContext);\n  const [ownSearch, setOwnSearch] = useState('');\n  const search = searchProp ?? ownSearch;\n  const [highlighted, setHighlighted] = useState<string | undefined>(undefined);\n  const registry = useRef(new Map<string, Registered>());\n  const [count, setCount] = useState(0);\n  const slots = commandVariants({ variant });\n\n  // The dialog sizes itself to the layout.\n  const setDialogVariant = dialog?.setVariant;\n  useEffect(() => {\n    setDialogVariant?.(variant);\n  }, [setDialogVariant, variant]);\n\n  const register = useCallback((value: string, data: Registered) => {\n    registry.current.set(value, data);\n    setCount(registry.current.size);\n    return () => {\n      registry.current.delete(value);\n      setCount(registry.current.size);\n    };\n  }, []);\n\n  const setSearch = (next: string) => {\n    if (searchProp === undefined) setOwnSearch(next);\n    onSearchChange?.(next);\n  };\n\n  const context = useMemo<CommandContextValue>(\n    () => ({\n      variant,\n      slots,\n      search,\n      filtering: filter,\n      columns,\n      highlighted,\n      register,\n      count,\n      close: dialog?.close ?? (() => {}),\n    }),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [variant, search, filter, columns, highlighted, register, count, dialog?.close]\n  );\n\n  return (\n    <CommandContext.Provider value={context}>\n      <Autocomplete.Root\n        inline\n        open\n        filter={null}\n        grid={variant === 'grid'}\n        value={search}\n        onValueChange={(next, details) => {\n          if (details.reason !== 'item-press') setSearch(next);\n        }}\n        onItemHighlighted={(value) => setHighlighted(typeof value === 'string' ? value : undefined)}\n        autoHighlight=\"always\"\n        keepHighlight\n        itemToStringValue={(value: string) => value}\n      >\n        <div\n          data-slot=\"command\"\n          data-variant={variant}\n          aria-label={label}\n          className={slots.root({\n            // Outside a dialog it draws the panel itself, so it can sit in the page.\n            className: [\n              !dialog && variant !== 'inline' ? 'border bg-popover shadow-xl shadow-black/10' : '',\n              !dialog && variant !== 'inline' && variant !== 'spotlight' ? 'rounded-2xl' : '',\n              className,\n            ],\n          })}\n          {...props}\n        >\n          {children}\n        </div>\n      </Autocomplete.Root>\n    </CommandContext.Provider>\n  );\n}\nCommandRoot.displayName = 'Command';\n\n/* -------------------------------------------------------------------------- */\n/* Field                                                                      */\n/* -------------------------------------------------------------------------- */\n\nfunction SearchGlyph() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n      <circle cx=\"11\" cy=\"11\" r=\"7\" />\n      <path d=\"m20 20-3.5-3.5\" />\n    </svg>\n  );\n}\n\nexport interface CommandInputProps extends Omit<ComponentProps<'input'>, 'value' | 'defaultValue' | 'prefix'> {\n  /** What sits before the field. Default a magnifying glass; pass `null` for none. */\n  icon?: ReactNode;\n  /** A chip before the text, such as the page a nested list is on. */\n  prefix?: ReactNode;\n  /** Something at the end of the field, such as a `Command.InputAction`. */\n  children?: ReactNode;\n  className?: string;\n}\n\n/** The field. It has focus while the launcher is open, and the arrow keys and Enter work from it. */\nfunction CommandInput({ icon, prefix, children, className, placeholder = 'Search…', ...props }: CommandInputProps) {\n  const { slots } = useCommand('Command.Input');\n  return (\n    <Autocomplete.InputGroup data-slot=\"command-input\" className={slots.inputRow()}>\n      {icon === null ? null : (\n        <span aria-hidden=\"true\" className={slots.inputIcon()}>\n          {icon ?? <SearchGlyph />}\n        </span>\n      )}\n      {prefix ? (\n        <span data-slot=\"command-prefix\" className={slots.prefix()}>\n          {prefix}\n        </span>\n      ) : null}\n      <Autocomplete.Input\n        aria-label={typeof placeholder === 'string' ? placeholder : 'Search'}\n        placeholder={placeholder}\n        className={slots.input({ className })}\n        {...(props as Autocomplete.Input.Props)}\n      />\n      {children}\n    </Autocomplete.InputGroup>\n  );\n}\nCommandInput.displayName = 'Command.Input';\n\nexport interface CommandKeysProps extends Omit<ComponentProps<'span'>, 'children'> {\n  /** The keys, each drawn as a key cap: `['⌘', 'K']`. */\n  keys: readonly string[];\n}\n\n/** Keys drawn as key caps. It only shows them; handle the keys where the action lives. */\nfunction CommandKeys({ keys, className, ...props }: CommandKeysProps) {\n  const { slots } = useCommand('Command.Keys');\n  return (\n    <span data-slot=\"command-keys\" className={slots.keys({ className })} {...props}>\n      {keys.map((key, index) => (\n        <kbd key={`${key}-${index}`} className={slots.key()}>\n          {key}\n        </kbd>\n      ))}\n    </span>\n  );\n}\nCommandKeys.displayName = 'Command.Keys';\n\nexport interface CommandInputActionProps extends ComponentProps<'button'> {\n  /** The keys that do the same, drawn after the label. */\n  keys?: readonly string[];\n}\n\n/** A button at the end of the field, such as switching the field to asking an assistant. */\nfunction CommandInputAction({ keys, className, children, type = 'button', ...props }: CommandInputActionProps) {\n  const { slots } = useCommand('Command.InputAction');\n  return (\n    <button type={type} data-slot=\"command-input-action\" className={slots.inputAction({ className })} {...props}>\n      {children}\n      {keys ? <CommandKeys keys={keys} /> : null}\n    </button>\n  );\n}\nCommandInputAction.displayName = 'Command.InputAction';\n\n/* -------------------------------------------------------------------------- */\n/* List                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface CommandListProps extends ComponentProps<'div'> {}\n\n/** The results, scrolling inside the launcher. In the `split` layout the preview of the highlighted item sits beside it. */\nfunction CommandList({ className, children, ...props }: CommandListProps) {\n  const command = useCommand('Command.List');\n  const list = (\n    <Autocomplete.List data-slot=\"command-list\" className={command.slots.list({ className })} {...props}>\n      {children}\n    </Autocomplete.List>\n  );\n  if (command.variant !== 'split') return list;\n  return (\n    <div className={command.slots.body()}>\n      {list}\n      <CommandPreviewPane />\n    </div>\n  );\n}\nCommandList.displayName = 'Command.List';\n\n/** Reads the preview of the highlighted item from the registry. */\nfunction CommandPreviewPane() {\n  const command = useCommand('Command.List');\n  return (\n    <aside aria-live=\"polite\" data-slot=\"command-preview\" className={command.slots.pane()}>\n      <PreviewOf value={command.highlighted} />\n    </aside>\n  );\n}\n\nconst PreviewContext = createContext<Map<string, ReactNode>>(new Map());\n\nfunction PreviewOf({ value }: { value: string | undefined }) {\n  const previews = useContext(PreviewContext);\n  return value ? (previews.get(value) ?? null) : null;\n}\n\nexport interface CommandGroupProps extends Omit<ComponentProps<'div'>, 'title'> {\n  /** The group's heading. */\n  heading?: ReactNode;\n}\n\n/** Items under a heading. It hides when none of its items match. */\nfunction CommandGroup({ heading, className, children, ...props }: CommandGroupProps) {\n  const command = useCommand('Command.Group');\n  const { slots } = command;\n\n  let content: ReactNode = children;\n  if (command.variant === 'grid') {\n    // Tiles go in rows so the arrow keys can move up and down as well as\n    // across. The rows are cut from the items that match, so a filtered grid\n    // closes up instead of leaving holes.\n    const items = Children.toArray(children).filter(\n      (child): child is ReactElement<CommandItemProps> =>\n        isValidElement<CommandItemProps>(child) &&\n        (!command.filtering || child.props.forceMount === true || matches(command.search, haystackOf(child.props)))\n    );\n    const rows: ReactElement<CommandItemProps>[][] = [];\n    for (let index = 0; index < items.length; index += command.columns) rows.push(items.slice(index, index + command.columns));\n    content = rows.map((row, index) => (\n      <Autocomplete.Row\n        key={index}\n        className={slots.gridRow()}\n        style={{ gridTemplateColumns: `repeat(${command.columns}, minmax(0, 1fr))` }}\n      >\n        {row}\n      </Autocomplete.Row>\n    ));\n  }\n\n  return (\n    <Autocomplete.Group data-slot=\"command-group\" className={slots.group({ className })} {...props}>\n      {heading ? <Autocomplete.GroupLabel className={slots.groupLabel()}>{heading}</Autocomplete.GroupLabel> : null}\n      {content}\n    </Autocomplete.Group>\n  );\n}\nCommandGroup.displayName = 'Command.Group';\n\nexport interface CommandItemProps extends Omit<ComponentProps<'div'>, 'onSelect' | 'title'> {\n  /** What identifies it. Default its text. Two items must not share one. */\n  value?: string;\n  /** The text it is matched against, when its children are not plain text. */\n  textValue?: string;\n  /** More words it should be found by: `['settings', 'preferences']`. */\n  keywords?: readonly string[];\n  /** An icon or an app's picture before the title. */\n  icon?: ReactNode;\n  /** Quieter words after the title: the app it belongs to, a handle. */\n  subtitle?: ReactNode;\n  /** What kind of thing it is, at the end of the row: `Command`, `Application`. */\n  kind?: ReactNode;\n  /** A short flag before the kind, such as `Pro`. */\n  badge?: ReactNode;\n  /** Keys that run it, drawn at the end of the row. */\n  shortcut?: readonly string[];\n  /** What the `split` layout shows beside the list while it is highlighted. */\n  preview?: ReactNode;\n  /** Called when it is pressed, or Enter is pressed while it is highlighted. */\n  onSelect?: () => void;\n  /** Whether choosing it closes the dialog it is in. Default `true`. */\n  closeOnSelect?: boolean;\n  /** Whether it shows whatever is typed. Use it for an item such as \"Search the web for …\". */\n  forceMount?: boolean;\n  /** Whether it can be chosen. */\n  disabled?: boolean;\n}\n\n/** One result. It hides itself when it does not match what is typed. */\nfunction CommandItem(props: CommandItemProps) {\n  const {\n    value: valueProp,\n    textValue,\n    keywords,\n    icon,\n    subtitle,\n    kind,\n    badge,\n    shortcut,\n    preview,\n    onSelect,\n    closeOnSelect = true,\n    forceMount = false,\n    disabled,\n    className,\n    children,\n    ...rest\n  } = props;\n  const command = useCommand('Command.Item');\n  const previews = useContext(PreviewContext);\n  const { slots } = command;\n  const value = valueProp ?? textValue ?? textOf(children);\n  const shown = forceMount || !command.filtering || matches(command.search, haystackOf(props));\n\n  const { register } = command;\n  useEffect(() => {\n    if (!shown) return;\n    if (preview !== undefined) previews.set(value, preview);\n    const unregister = register(value, { title: children });\n    return () => {\n      previews.delete(value);\n      unregister();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [shown, value, register]);\n\n  useEffect(() => {\n    if (shown && preview !== undefined) previews.set(value, preview);\n  }, [shown, preview, previews, value]);\n\n  if (!shown) return null;\n\n  const end = kind || badge || shortcut;\n  return (\n    <Autocomplete.Item\n      value={value}\n      disabled={disabled}\n      data-slot=\"command-item\"\n      className={slots.item({ className })}\n      onClick={() => {\n        onSelect?.();\n        if (closeOnSelect) command.close();\n      }}\n      {...(rest as Autocomplete.Item.Props)}\n    >\n      {icon ? (\n        <span aria-hidden=\"true\" className={slots.itemIcon()}>\n          {icon}\n        </span>\n      ) : null}\n      <span className={slots.itemTitle()}>{children}</span>\n      {subtitle ? <span className={slots.itemSubtitle()}>{subtitle}</span> : null}\n      {end ? (\n        <span className={slots.itemEnd()}>\n          {badge ? <span className={slots.badge()}>{badge}</span> : null}\n          {kind ? <span>{kind}</span> : null}\n          {shortcut ? <CommandKeys keys={shortcut} /> : null}\n        </span>\n      ) : null}\n    </Autocomplete.Item>\n  );\n}\nCommandItem.displayName = 'Command.Item';\n\nexport interface CommandEmptyProps extends ComponentProps<'div'> {}\n\n/** Shown when nothing matches what is typed. */\nfunction CommandEmpty({ className, children = 'No results', ...props }: CommandEmptyProps) {\n  const command = useCommand('Command.Empty');\n  if (command.count > 0) return null;\n  return (\n    <div role=\"status\" data-slot=\"command-empty\" className={command.slots.empty({ className })} {...props}>\n      {children}\n    </div>\n  );\n}\nCommandEmpty.displayName = 'Command.Empty';\n\nexport interface CommandLoadingProps extends ComponentProps<'div'> {}\n\n/** A spinner and a line, for results still on their way. */\nfunction CommandLoading({ className, children = 'Searching…', ...props }: CommandLoadingProps) {\n  const { slots } = useCommand('Command.Loading');\n  return (\n    <div data-slot=\"command-loading\" className={slots.loading({ className })} {...props}>\n      <Spinner size=\"sm\" label={typeof children === 'string' ? children : 'Loading'} />\n      {children}\n    </div>\n  );\n}\nCommandLoading.displayName = 'Command.Loading';\n\nexport interface CommandSeparatorProps extends ComponentProps<'div'> {}\n\n/** A rule between groups. */\nfunction CommandSeparator({ className, ...props }: CommandSeparatorProps) {\n  const { slots } = useCommand('Command.Separator');\n  return <div role=\"separator\" data-slot=\"command-separator\" className={slots.separator({ className })} {...props} />;\n}\nCommandSeparator.displayName = 'Command.Separator';\n\n/* -------------------------------------------------------------------------- */\n/* Footer                                                                     */\n/* -------------------------------------------------------------------------- */\n\nexport interface CommandFooterProps extends ComponentProps<'div'> {\n  /** What sits at the start of the bar: where the launcher is, or a hint. */\n  lead?: ReactNode;\n}\n\n/** The bar at the foot of the launcher, with its actions at the end. */\nfunction CommandFooter({ lead, className, children, ...props }: CommandFooterProps) {\n  const { slots } = useCommand('Command.Footer');\n  return (\n    <div data-slot=\"command-footer\" className={slots.footer({ className })} {...props}>\n      <span className={slots.footerLead()}>{lead}</span>\n      {children ? <span className={slots.footerBar()}>{children}</span> : null}\n    </div>\n  );\n}\nCommandFooter.displayName = 'Command.Footer';\n\nexport interface CommandFooterActionProps extends ComponentProps<'button'> {\n  /** The keys that do the same, drawn after the label. */\n  keys?: readonly string[];\n}\n\n/** An action in the footer, with its keys after its label. The first is usually what Enter does. */\nfunction CommandFooterAction({ keys, className, children, type = 'button', ...props }: CommandFooterActionProps) {\n  const { slots } = useCommand('Command.FooterAction');\n  return (\n    <button type={type} data-slot=\"command-footer-action\" className={slots.footerAction({ className })} {...props}>\n      {children}\n      {keys ? <CommandKeys keys={keys} /> : null}\n    </button>\n  );\n}\nCommandFooterAction.displayName = 'Command.FooterAction';\n\nexport interface CommandFooterButtonProps extends ComponentProps<'button'> {\n  /** What a screen reader calls it, since it shows only an icon. */\n  'aria-label': string;\n}\n\n/** An icon button for the footer's start, such as one that opens a menu of settings. Round and floating in `spotlight`. */\nfunction CommandFooterButton({ className, type = 'button', ...props }: CommandFooterButtonProps) {\n  const { slots } = useCommand('Command.FooterButton');\n  return <button type={type} data-slot=\"command-footer-button\" className={slots.footerButton({ className })} {...props} />;\n}\nCommandFooterButton.displayName = 'Command.FooterButton';\n\n/** A thin rule between two footer actions. */\nfunction CommandFooterDivider() {\n  const { slots } = useCommand('Command.FooterDivider');\n  return <span aria-hidden=\"true\" className={slots.footerDivider()} />;\n}\nCommandFooterDivider.displayName = 'Command.FooterDivider';\n\n/* -------------------------------------------------------------------------- */\n/* Dialog                                                                     */\n/* -------------------------------------------------------------------------- */\n\nfunction isApple() {\n  return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);\n}\n\nfunction hotkeyMatches(event: globalThis.KeyboardEvent, hotkey: string) {\n  const parts = hotkey.toLowerCase().split('+').map((part) => part.trim());\n  const key = parts[parts.length - 1] ?? '';\n  const mod = parts.includes('mod');\n  const pressedMod = isApple() ? event.metaKey : event.ctrlKey;\n  const code = /^[a-z]$/.test(key) ? `Key${key.toUpperCase()}` : /^[0-9]$/.test(key) ? `Digit${key}` : null;\n  const keyMatches = code ? event.code === code : event.key.toLowerCase() === key;\n  return keyMatches && mod === pressedMod && parts.includes('shift') === event.shiftKey && parts.includes('alt') === event.altKey;\n}\n\nexport interface CommandDialogProps {\n  /** Whether it is open, when the caller holds it. */\n  open?: boolean;\n  /** Whether it is open to begin with. */\n  defaultOpen?: boolean;\n  /** Called when it opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Keys that open and close it from anywhere on the page. `mod` is ⌘ on a Mac and Ctrl elsewhere. Default `mod+k`; `null` for none. */\n  hotkey?: string | null;\n  /** A button that opens it, such as a search field in a header. */\n  trigger?: ReactElement;\n  /** What a screen reader calls the dialog. Default `Command menu`. */\n  label?: string;\n  /** Put a `Command` in it. */\n  children?: ReactNode;\n  /** Classes for the panel. */\n  className?: string;\n}\n\n/** Opens a `Command` in a panel over the page, from its hotkey or its trigger. Choosing an item closes it. */\nfunction CommandDialog({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  hotkey = 'mod+k',\n  trigger,\n  label = 'Command menu',\n  children,\n  className,\n}: CommandDialogProps) {\n  const [ownOpen, setOwnOpen] = useState(defaultOpen);\n  const [instant, setInstant] = useState(false);\n  const [variant, setVariant] = useState<CommandVariant | null>(null);\n  const open = openProp ?? ownOpen;\n  const slots = commandVariants();\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (openProp === undefined) setOwnOpen(next);\n      onOpenChange?.(next);\n    },\n    [openProp, onOpenChange]\n  );\n\n  useEffect(() => {\n    if (!hotkey) return;\n    function onKeyDown(event: globalThis.KeyboardEvent) {\n      if (!hotkeyMatches(event, hotkey!)) return;\n      event.preventDefault();\n      setInstant(true);\n      setOpen(!open);\n    }\n    window.addEventListener('keydown', onKeyDown);\n    return () => window.removeEventListener('keydown', onKeyDown);\n  }, [hotkey, open, setOpen]);\n\n  const dialog = useMemo(() => ({ close: () => setOpen(false), variant, setVariant }), [setOpen, variant]);\n\n  return (\n    <DialogContext.Provider value={dialog}>\n      <Dialog.Root\n        open={open}\n        onOpenChange={(next, details) => {\n          // Escape is a key too: a launcher closed from the keyboard goes at once.\n          setInstant(details.reason === 'escape-key');\n          setOpen(next);\n        }}\n      >\n        {trigger ? <Dialog.Trigger render={trigger} onClick={() => setInstant(false)} /> : null}\n        <Dialog.Portal>\n          <div data-instant={instant ? '' : undefined} className=\"contents\">\n            <Dialog.Backdrop className={slots.backdrop()} />\n            <Dialog.Viewport className={slots.viewport()}>\n              <Dialog.Popup\n                aria-label={label}\n                data-slot=\"command-dialog\"\n                data-variant={variant ?? 'spotlight'}\n                data-instant={instant ? '' : undefined}\n                className={slots.popup({ className: [DIALOG_WIDTH[variant ?? 'spotlight'], className] })}\n              >\n                {children}\n              </Dialog.Popup>\n            </Dialog.Viewport>\n          </div>\n        </Dialog.Portal>\n      </Dialog.Root>\n    </DialogContext.Provider>\n  );\n}\nCommandDialog.displayName = 'Command.Dialog';\n\n/* -------------------------------------------------------------------------- */\n/* Previews                                                                   */\n/* -------------------------------------------------------------------------- */\n\n/** Holds what each item previews, for the `split` layout. One per `Command`. */\nfunction PreviewStore({ children }: { children: ReactNode }) {\n  const [store] = useState(() => new Map<string, ReactNode>());\n  return <PreviewContext.Provider value={store}>{children}</PreviewContext.Provider>;\n}\n\nfunction CommandWithPreviews(props: CommandProps) {\n  return (\n    <PreviewStore>\n      <CommandRoot {...props} />\n    </PreviewStore>\n  );\n}\nCommandWithPreviews.displayName = 'Command';\n\nexport const Command = Object.assign(CommandWithPreviews, {\n  Dialog: CommandDialog,\n  Input: CommandInput,\n  InputAction: CommandInputAction,\n  List: CommandList,\n  Group: CommandGroup,\n  Item: CommandItem,\n  Empty: CommandEmpty,\n  Loading: CommandLoading,\n  Separator: CommandSeparator,\n  Footer: CommandFooter,\n  FooterAction: CommandFooterAction,\n  FooterButton: CommandFooterButton,\n  FooterDivider: CommandFooterDivider,\n  Keys: CommandKeys,\n});\n\nexport {\n  CommandDialog,\n  CommandInput,\n  CommandInputAction,\n  CommandList,\n  CommandGroup,\n  CommandItem,\n  CommandEmpty,\n  CommandLoading,\n  CommandSeparator,\n  CommandFooter,\n  CommandFooterAction,\n  CommandFooterButton,\n  CommandFooterDivider,\n  CommandKeys,\n};\n"
    }
  ]
}
