# Command

> A searchable list of actions, inline or as a ⌘K palette, with pages, shortcuts and highlighted matches.

Docs: https://hextaui.com/docs/command
Markdown: https://hextaui.com/docs/command.md

```tsx title="components/examples/command/demo.tsx"
import {
  IconCalculator,
  IconCalendar,
  IconCreditCard,
  IconMoodSmile,
  IconSettings,
  IconUser,
} from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandDemo() {
  return (
    <Command highlight className="w-full max-w-sm">
      <CommandInput placeholder="Type a command or search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <IconCalendar />
            Calendar
          </CommandItem>
          <CommandItem>
            <IconMoodSmile />
            Search Emoji
          </CommandItem>
          <CommandItem disabled>
            <IconCalculator />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem shortcut="mod+p">
            <IconUser />
            Profile
          </CommandItem>
          <CommandItem shortcut="mod+b">
            <IconCreditCard />
            Billing
          </CommandItem>
          <CommandItem shortcut="mod+,">
            <IconSettings />
            Settings
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/command.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cmdk cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/command.tsx"
"use client"

import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { IconChevronLeft, IconSearch, IconX } from "@tabler/icons-react"
import { Command as CommandPrimitive, useCommandState } from "cmdk"
import { cn } from "cn"

import { Spinner } from "@/components/ui/spinner"
import { formatHotkey, matchesHotkey, useIsApple } from "@/lib/hotkey"

import { Button } from "@/components/ui/button"
import { easeOut, prefersReducedMotion } from "@/lib/motion"

type CommandPageEntry = { id: string; title: string }

type CommandContextValue = {
  search: string
  setSearch: (search: string) => void
  pages: CommandPageEntry[]
  direction: 1 | -1
  pushPage: (id: string, title?: string) => void
  popPage: (count?: number) => void
  registerShortcut: (hotkey: string, run: () => void) => () => void
  takeKeyboardSelect: () => { fromKeyboard: boolean; newTab: boolean }
}

const CommandContext = React.createContext<CommandContextValue | null>(null)
const CommandPageContext = React.createContext<string | null>(null)

function useCommandContext(part: string) {
  const context = React.useContext(CommandContext)
  if (!context) {
    throw new Error(`<${part}> must be used within <Command>.`)
  }
  return context
}

function useCommandPages() {
  const { pages, pushPage, popPage } = useCommandContext("useCommandPages")
  return {
    pages,
    page: pages.at(-1)?.id ?? null,
    push: pushPage,
    pop: popPage,
    reset: () => popPage(pages.length),
  }
}

function usePageVisible() {
  const context = React.useContext(CommandContext)
  const page = React.useContext(CommandPageContext)
  const current = context?.pages.at(-1)?.id ?? null
  return page === current
}

function subscribeNothing() {
  return () => {}
}

function isEditable(target: EventTarget | null) {
  return (
    target instanceof HTMLElement &&
    (target.isContentEditable ||
      target.closest("input, textarea, select, [contenteditable='true']") !==
        null)
  )
}

const internalEvents = new WeakSet<Event>()

const highlightName = "command-match"
const highlightRanges = new Map<string, Range[]>()
let highlightSheet: CSSStyleSheet | null = null

function adoptHighlightStyle() {
  if (
    highlightSheet ||
    typeof CSSStyleSheet === "undefined" ||
    !("adoptedStyleSheets" in document)
  ) {
    return
  }
  highlightSheet = new CSSStyleSheet()
  highlightSheet.replaceSync(
    `::highlight(${highlightName}) { color: var(--foreground); }`
  )
  document.adoptedStyleSheets = [...document.adoptedStyleSheets, highlightSheet]
}

function publishHighlights() {
  if (typeof CSS === "undefined" || !("highlights" in CSS)) {
    return
  }
  adoptHighlightStyle()
  const ranges = [...highlightRanges.values()].flat()
  if (ranges.length === 0) {
    CSS.highlights.delete(highlightName)
  } else {
    CSS.highlights.set(highlightName, new Highlight(...ranges))
  }
}

function matchIndices(text: string, query: string) {
  const haystack = text.toLowerCase()
  const needle = query.toLowerCase()
  const start = haystack.indexOf(needle)
  if (start !== -1) {
    return Array.from({ length: needle.length }, (_, index) => start + index)
  }
  const indices: number[] = []
  let from = 0
  for (const char of needle) {
    if (char === " ") {
      continue
    }
    const found = haystack.indexOf(char, from)
    if (found === -1) {
      return []
    }
    indices.push(found)
    from = found + 1
  }
  return indices
}

function highlightItem(item: HTMLElement, query: string) {
  const walker = document.createTreeWalker(item, NodeFilter.SHOW_TEXT, {
    acceptNode: (node) =>
      node.parentElement?.closest(
        "[data-slot=command-shortcut], svg, [aria-hidden=true]"
      )
        ? NodeFilter.FILTER_REJECT
        : NodeFilter.FILTER_ACCEPT,
  })
  const nodes: { node: Text; start: number }[] = []
  let text = ""
  for (let node = walker.nextNode(); node; node = walker.nextNode()) {
    nodes.push({ node: node as Text, start: text.length })
    text += node.textContent ?? ""
  }
  const ranges: Range[] = []
  for (const index of matchIndices(text, query.trim())) {
    const owner = nodes.findLast((entry) => entry.start <= index)
    if (!owner) {
      continue
    }
    const range = document.createRange()
    range.setStart(owner.node, index - owner.start)
    range.setEnd(owner.node, index - owner.start + 1)
    ranges.push(range)
  }
  return ranges
}

function CommandHighlighter() {
  const id = React.useId()
  const search = useCommandState((state) => state.search)
  const count = useCommandState((state) => state.filtered.count)
  const anchorRef = React.useRef<HTMLSpanElement>(null)

  React.useLayoutEffect(() => {
    const root = anchorRef.current?.closest<HTMLElement>("[cmdk-root]")
    if (!root) {
      return
    }
    const query = search.trim()
    root.toggleAttribute("data-highlighting", query !== "")
    highlightRanges.set(
      id,
      query
        ? Array.from(root.querySelectorAll<HTMLElement>("[cmdk-item]")).flatMap(
            (item) => highlightItem(item, query)
          )
        : []
    )
    publishHighlights()
  }, [count, id, search])

  React.useEffect(
    () => () => {
      highlightRanges.delete(id)
      publishHighlights()
    },
    [id]
  )

  return <span ref={anchorRef} hidden />
}

function defaultFormatResults(count: number) {
  if (count === 0) {
    return "No results"
  }
  return count === 1 ? "1 result" : `${count} results`
}

function CommandAnnouncer({
  format,
  pageTitle,
}: {
  format: (count: number) => string
  pageTitle: string
}) {
  const search = useCommandState((state) => state.search)
  const count = useCommandState((state) => state.filtered.count)
  const [message, setMessage] = React.useState("")
  const [previousPage, setPreviousPage] = React.useState(pageTitle)
  const [pageMessage, setPageMessage] = React.useState("")

  if (pageTitle !== previousPage) {
    setPreviousPage(pageTitle)
    setPageMessage(pageTitle)
  }

  React.useEffect(() => {
    const timer = setTimeout(
      () => setMessage(search ? format(count) : ""),
      search ? 500 : 0
    )
    return () => clearTimeout(timer)
  }, [count, format, search])

  return (
    <span
      data-slot="command-announcer"
      role="status"
      aria-live="polite"
      aria-atomic
      className="sr-only"
    >
      {message || pageMessage}
    </span>
  )
}

type CommandProps = React.ComponentProps<typeof CommandPrimitive> & {
  formatResults?: (count: number) => string
  highlight?: boolean
  rootTitle?: string
}

function Command({
  className,
  children,
  label = "Command menu",
  formatResults = defaultFormatResults,
  highlight = false,
  rootTitle = "All commands",
  onKeyDown,
  ...props
}: CommandProps) {
  const [search, setSearch] = React.useState("")
  const [pages, setPages] = React.useState<CommandPageEntry[]>([])
  const [direction, setDirection] = React.useState<1 | -1>(1)
  const shortcutsRef = React.useRef(
    new Map<symbol, { hotkey: string; run: () => void }>()
  )
  const enterRef = React.useRef({ at: -Infinity, newTab: false })

  const pushPage = React.useCallback((id: string, title?: string) => {
    setDirection(1)
    setPages((current) => [...current, { id, title: title ?? id }])
    setSearch("")
  }, [])

  const popPage = React.useCallback((count = 1) => {
    setDirection(-1)
    setPages((current) => current.slice(0, Math.max(0, current.length - count)))
    setSearch("")
  }, [])

  const registerShortcut = React.useCallback(
    (hotkey: string, run: () => void) => {
      const key = Symbol(hotkey)
      shortcutsRef.current.set(key, { hotkey, run })
      return () => {
        shortcutsRef.current.delete(key)
      }
    },
    []
  )

  const takeKeyboardSelect = React.useCallback(() => {
    const entry = enterRef.current
    const fromKeyboard = performance.now() - entry.at < 100
    enterRef.current = { at: -Infinity, newTab: false }
    return { fromKeyboard, newTab: fromKeyboard && entry.newTab }
  }, [])

  const context = React.useMemo(
    () => ({
      search,
      setSearch,
      pages,
      direction,
      pushPage,
      popPage,
      registerShortcut,
      takeKeyboardSelect,
    }),
    [
      direction,
      pages,
      popPage,
      pushPage,
      registerShortcut,
      search,
      takeKeyboardSelect,
    ]
  )

  return (
    <CommandContext.Provider value={context}>
      <CommandPrimitive
        data-slot="command"
        label={label}
        onKeyDown={(event) => {
          if (internalEvents.has(event.nativeEvent)) {
            return
          }
          onKeyDown?.(event)
          if (event.defaultPrevented || event.nativeEvent.isComposing) {
            return
          }
          if (event.key === "Enter") {
            enterRef.current = {
              at: performance.now(),
              newTab: event.metaKey || event.ctrlKey,
            }
            return
          }
          if (event.repeat) {
            return
          }
          for (const { hotkey, run } of shortcutsRef.current.values()) {
            if (matchesHotkey(event.nativeEvent, hotkey)) {
              event.preventDefault()
              run()
              return
            }
          }
        }}
        className={cn(
          "flex size-full min-w-0 flex-col overflow-hidden rounded-(--command-radius) bg-popover text-popover-foreground ring-(length:--hairline) ring-foreground/10 outline-none [--command-inset:--spacing(1.5)] [--command-radius:var(--radius-xl)] focus-visible:outline-hidden in-data-[slot=command-dialog]:rounded-none in-data-[slot=command-dialog]:ring-0 forced-colors:border",
          className
        )}
        {...props}
      >
        {children}
        {highlight ? <CommandHighlighter /> : null}
        <CommandAnnouncer
          format={formatResults}
          pageTitle={pages.at(-1)?.title ?? (pages.length ? "" : rootTitle)}
        />
      </CommandPrimitive>
    </CommandContext.Provider>
  )
}

function CommandPage({
  id,
  children,
}: {
  id: string
  children?: React.ReactNode
}) {
  return (
    <CommandPageContext.Provider value={id}>
      {children}
    </CommandPageContext.Provider>
  )
}

type CommandInputProps = Omit<
  React.ComponentProps<typeof CommandPrimitive.Input>,
  "value" | "onValueChange"
> & {
  value?: string
  onValueChange?: (search: string) => void
  clearLabel?: string
  backLabel?: (title: string) => string
}

function CommandInput({
  className,
  value,
  onValueChange,
  onKeyDown,
  clearLabel = "Clear search",
  backLabel = (title) => `Back from ${title}`,
  ref,
  ...props
}: CommandInputProps) {
  const context = useCommandContext("CommandInput")
  const search = value ?? context.search
  const inputRef = React.useRef<HTMLInputElement | null>(null)
  const page = context.pages.at(-1)

  const setInput = React.useCallback(
    (node: HTMLInputElement | null) => {
      inputRef.current = node
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    },
    [ref]
  )

  const change = (next: string) => {
    if (value === undefined) {
      context.setSearch(next)
    }
    onValueChange?.(next)
  }

  return (
    <div
      data-slot="command-input-wrapper"
      className="flex shrink-0 items-center gap-2.5 border-b ps-3.5 pe-2"
    >
      {page ? (
        <button
          type="button"
          data-slot="command-page-chip"
          aria-label={backLabel(page.title)}
          onClick={() => {
            context.popPage()
            inputRef.current?.focus()
          }}
          className="-ms-1.5 inline-flex h-6 max-w-40 min-w-0 shrink-0 items-center gap-0.5 rounded-[calc(var(--radius-sm)*0.8)] bg-muted ps-0.5 pe-2 text-xs font-medium text-foreground transition-[background-color,scale] duration-150 ease-out-quint outline-none hover:bg-foreground/10 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden motion-safe:animate-in motion-safe:animation-duration-150 motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:active:scale-95 pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5"
        >
          <IconChevronLeft
            aria-hidden
            className="size-3.5 shrink-0 rtl:-scale-x-100"
          />
          <span className="truncate">{page.title}</span>
        </button>
      ) : (
        <IconSearch
          aria-hidden
          className="pointer-events-none size-4 shrink-0 text-muted-foreground"
        />
      )}
      <CommandPrimitive.Input
        ref={setInput}
        data-slot="command-input"
        value={search}
        onValueChange={change}
        onKeyDown={(event) => {
          onKeyDown?.(event)
          if (event.defaultPrevented || event.nativeEvent.isComposing) {
            return
          }
          if (event.key === "Escape") {
            if (search !== "") {
              event.preventDefault()
              change("")
            } else if (context.pages.length > 0) {
              event.preventDefault()
              context.popPage()
            }
            return
          }
          if (
            event.key === "Backspace" &&
            search === "" &&
            context.pages.length > 0 &&
            !event.repeat
          ) {
            event.preventDefault()
            context.popPage()
          }
        }}
        className={cn(
          "h-11 w-full min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:h-12 pointer-coarse:text-[max(16px,1rem)] [&::-webkit-search-cancel-button]:hidden",
          className
        )}
        {...props}
      />
      <Button
        type="button"
        variant="ghost"
        size="icon-xs"
        tabIndex={-1}
        aria-label={clearLabel}
        aria-hidden={search === "" || undefined}
        data-slot="command-clear"
        onClick={() => {
          change("")
          inputRef.current?.focus()
        }}
        className={cn(
          "shrink-0 rounded-full text-muted-foreground transition-[opacity,scale,color,background-color] duration-150 hover:text-foreground",
          search === ""
            ? "pointer-events-none opacity-0 motion-safe:scale-75"
            : "opacity-100"
        )}
      >
        <IconX />
      </Button>
    </div>
  )
}

function scrollIntoList(element: HTMLElement) {
  const list = element.closest<HTMLElement>("[cmdk-list]")
  if (!list) {
    return
  }
  const style = getComputedStyle(list)
  const top = parseFloat(style.scrollPaddingTop) || 0
  const bottom = parseFloat(style.scrollPaddingBottom) || 0
  const listRect = list.getBoundingClientRect()
  const rect = element.getBoundingClientRect()
  if (rect.top < listRect.top + top) {
    list.scrollTop -= listRect.top + top - rect.top
  } else if (rect.bottom > listRect.bottom - bottom) {
    list.scrollTop += rect.bottom - (listRect.bottom - bottom)
  }
}

function useScrollWithinList(
  ref: React.RefObject<HTMLElement | null>,
  selector?: string
) {
  React.useLayoutEffect(() => {
    const root = ref.current
    const target = selector ? root?.querySelector<HTMLElement>(selector) : root
    if (!target) {
      return
    }
    target.scrollIntoView = () => scrollIntoList(target)
    return () => {
      delete (target as Partial<HTMLElement>).scrollIntoView
    }
  })
}

function CommandList({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
  const context = useCommandContext("CommandList")
  const listRef = React.useRef<HTMLDivElement>(null)
  const depth = context.pages.length
  const [previousDepth, setPreviousDepth] = React.useState(depth)
  const [pageTurn, setPageTurn] = React.useState(0)

  if (depth !== previousDepth) {
    setPreviousDepth(depth)
    setPageTurn((turn) => turn + 1)
  }

  React.useLayoutEffect(() => {
    const list = listRef.current
    if (!pageTurn || !list) {
      return
    }
    const home = new KeyboardEvent("keydown", {
      key: "Home",
      bubbles: true,
      cancelable: true,
    })
    internalEvents.add(home)
    list.closest("[cmdk-root]")?.dispatchEvent(home)
    list.scrollTop = 0

    const sizer = list.querySelector<HTMLElement>(":scope > [cmdk-list-sizer]")
    if (!sizer || typeof sizer.animate !== "function") {
      return
    }
    const rtl = getComputedStyle(sizer).direction === "rtl"
    const offset = 16 * context.direction * (rtl ? -1 : 1)
    const animation = sizer.animate(
      prefersReducedMotion()
        ? [{ opacity: 0 }, { opacity: 1 }]
        : [
            { translate: `${offset}px 0`, opacity: 0 },
            { translate: "0 0", opacity: 1 },
          ],
      { duration: 180, easing: easeOut }
    )
    return () => animation.cancel()
  }, [context.direction, pageTurn])

  React.useLayoutEffect(() => {
    const list = listRef.current
    if (!list || typeof ResizeObserver !== "function") {
      return
    }
    let frame = 0
    const settle = () => {
      cancelAnimationFrame(frame)
      frame = requestAnimationFrame(() => {
        frame = requestAnimationFrame(() => {
          list.setAttribute("data-settled", "")
        })
      })
    }
    const observer = new ResizeObserver(() => {
      if (list.getClientRects().length === 0) {
        cancelAnimationFrame(frame)
        list.removeAttribute("data-settled")
      } else if (!list.hasAttribute("data-settled")) {
        settle()
      }
    })
    observer.observe(list)
    return () => {
      cancelAnimationFrame(frame)
      observer.disconnect()
      list.removeAttribute("data-settled")
    }
  }, [])

  return (
    <CommandPrimitive.List
      ref={listRef}
      data-slot="command-list"
      className={cn(
        "max-h-[min(24rem,45dvh)] scroll-py-(--command-inset) [scrollbar-gutter:stable] overflow-x-hidden overflow-y-auto overscroll-none duration-150 ease-out-quint outline-none focus-visible:outline-hidden data-settled:h-(--cmdk-list-height) motion-safe:data-settled:transition-[height] motion-reduce:transition-none [&>[cmdk-list-sizer]]:p-(--command-inset)",
        className
      )}
      {...props}
    />
  )
}

type CommandEmptyProps = Omit<
  React.ComponentProps<typeof CommandPrimitive.Empty>,
  "children"
> & {
  children?: React.ReactNode | ((search: string) => React.ReactNode)
}

function CommandEmpty({ className, children, ...props }: CommandEmptyProps) {
  const search = useCommandState((state) => state.search)
  const hydrating = React.useSyncExternalStore(
    subscribeNothing,
    () => false,
    () => true
  )

  if (hydrating) {
    return null
  }

  return (
    <CommandPrimitive.Empty
      data-slot="command-empty"
      className={cn(
        "px-3 py-8 text-center text-sm text-balance text-muted-foreground in-[[cmdk-list]:has([cmdk-loading])]:hidden",
        className
      )}
      {...props}
    >
      {typeof children === "function" ? children(search) : children}
    </CommandPrimitive.Empty>
  )
}

function useCommandLoading(
  loading: boolean,
  {
    delay = 150,
    minDuration = 300,
  }: { delay?: number; minDuration?: number } = {}
) {
  const [visible, setVisible] = React.useState(false)
  const shownAtRef = React.useRef(0)

  React.useEffect(() => {
    if (loading && !visible) {
      const timer = setTimeout(() => {
        shownAtRef.current = performance.now()
        setVisible(true)
      }, delay)
      return () => clearTimeout(timer)
    }
    if (!loading && visible) {
      const remaining = Math.max(
        0,
        minDuration - (performance.now() - shownAtRef.current)
      )
      const timer = setTimeout(() => setVisible(false), remaining)
      return () => clearTimeout(timer)
    }
  }, [delay, loading, minDuration, visible])

  return visible
}

type CommandLoadingProps = React.ComponentProps<
  typeof CommandPrimitive.Loading
> & {
  loading?: boolean
  delay?: number
  minDuration?: number
}

function CommandLoading({
  className,
  children,
  label,
  loading = true,
  delay,
  minDuration,
  ...props
}: CommandLoadingProps) {
  const visible = useCommandLoading(loading, { delay, minDuration })

  if (!loading && !visible) {
    return null
  }

  return (
    <CommandPrimitive.Loading
      data-slot="command-loading"
      data-pending={visible ? undefined : ""}
      label={label ?? (typeof children === "string" ? children : undefined)}
      className={cn(
        "flex min-h-9 items-center justify-center px-3 py-4 text-sm text-muted-foreground data-pending:sr-only motion-safe:animate-in motion-safe:animation-duration-150 motion-safe:fade-in-0 [&>div]:flex [&>div]:items-center [&>div]:gap-2",
        className
      )}
      {...props}
    >
      <Spinner aria-hidden />
      {children}
    </CommandPrimitive.Loading>
  )
}

function CommandGroup({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
  const groupRef = React.useRef<HTMLDivElement>(null)
  useScrollWithinList(groupRef, ":scope > [cmdk-group-heading]")

  if (!usePageVisible()) {
    return null
  }

  return (
    <CommandPrimitive.Group
      ref={groupRef}
      data-slot="command-group"
      className={cn(
        "overflow-hidden text-foreground not-first:mt-1 [&>[cmdk-group-heading]]:px-2.5 [&>[cmdk-group-heading]]:pt-2 [&>[cmdk-group-heading]]:pb-1.5 [&>[cmdk-group-heading]]:text-xs [&>[cmdk-group-heading]]:font-medium [&>[cmdk-group-heading]]:text-muted-foreground [&>[cmdk-group-heading]]:select-none",
        className
      )}
      {...props}
    />
  )
}

function CommandSeparator({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
  if (!usePageVisible()) {
    return null
  }

  return (
    <CommandPrimitive.Separator
      data-slot="command-separator"
      aria-hidden
      className={cn(
        "-mx-(--command-inset) my-(--command-inset) h-(length:--hairline) bg-border",
        className
      )}
      {...props}
    />
  )
}

function textOf(node: React.ReactNode): string {
  if (typeof node === "string" || typeof node === "number") {
    return String(node)
  }
  if (Array.isArray(node)) {
    return node.map(textOf).join("")
  }
  if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
    return node.type === CommandShortcut ? "" : textOf(node.props.children)
  }
  return ""
}

const confirmBlink = 60
const confirmDelay = 120

type CommandItemProps = Omit<
  React.ComponentProps<typeof CommandPrimitive.Item>,
  "asChild"
> & {
  shortcut?: string
  page?: string
  pageTitle?: string
  href?: string
  render?: React.ReactElement<{ href?: string }>
  confirm?: boolean
}

function CommandItem({
  className,
  children,
  shortcut,
  page,
  pageTitle,
  href,
  render,
  confirm = true,
  onSelect,
  disabled,
  value,
  ...props
}: CommandItemProps) {
  const context = useCommandContext("CommandItem")
  const visible = usePageVisible()
  const apple = useIsApple()
  const itemRef = React.useRef<HTMLDivElement>(null)
  useScrollWithinList(itemRef)
  const [confirming, setConfirming] = React.useState(false)
  const busyRef = React.useRef(false)
  const timersRef = React.useRef<ReturnType<typeof setTimeout>[]>([])
  const linkHref = href ?? render?.props.href

  React.useEffect(() => {
    const timers = timersRef
    return () => timers.current.forEach(clearTimeout)
  }, [])

  const act = (
    value: string,
    keyboard: { fromKeyboard: boolean; newTab: boolean }
  ) => {
    busyRef.current = false
    if (page) {
      context.pushPage(page, pageTitle ?? value)
    } else if (linkHref && keyboard.fromKeyboard) {
      if (keyboard.newTab) {
        window.open(linkHref, "_blank", "noopener")
      } else {
        itemRef.current?.click()
      }
    }
    onSelect?.(value)
  }

  const select = (value: string) => {
    if (busyRef.current) {
      return
    }
    const keyboard = context.takeKeyboardSelect()
    if (!confirm || prefersReducedMotion()) {
      act(value, keyboard)
      return
    }
    busyRef.current = true
    setConfirming(true)
    timersRef.current.push(
      setTimeout(() => setConfirming(false), confirmBlink),
      setTimeout(() => act(value, keyboard), confirmDelay)
    )
  }

  const selectRef = React.useRef(select)
  React.useLayoutEffect(() => {
    selectRef.current = select
  })

  React.useEffect(() => {
    if (!shortcut || disabled) {
      return
    }
    return context.registerShortcut(shortcut, () => {
      const value =
        itemRef.current?.getAttribute("data-value") ??
        itemRef.current?.textContent ??
        ""
      selectRef.current(value)
    })
  }, [context, disabled, shortcut])

  if (!visible) {
    return null
  }

  const content = (
    <>
      {children}
      {page ? (
        <IconChevronLeft
          aria-hidden
          className="ms-auto size-4 shrink-0 rotate-180 text-muted-foreground rtl:rotate-0"
        />
      ) : null}
      {shortcut ? (
        <CommandShortcut aria-hidden>
          {formatHotkey(shortcut, apple)}
        </CommandShortcut>
      ) : null}
    </>
  )

  const link = render ?? (href ? <a href={href} /> : null)

  return (
    <CommandPrimitive.Item
      ref={itemRef}
      data-slot="command-item"
      data-confirming={confirming ? "" : undefined}
      data-page={page ? "" : undefined}
      aria-keyshortcuts={
        shortcut
          ? shortcut
              .split("+")
              .map((part) =>
                part === "mod" ? (apple ? "Meta" : "Control") : part
              )
              .join("+")
          : undefined
      }
      value={value ?? (textOf(children).trim() || undefined)}
      disabled={disabled}
      onSelect={select}
      asChild={Boolean(link)}
      className={cn(
        "relative flex min-h-9 min-w-0 cursor-default items-center gap-2.5 rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--command-radius)-var(--command-inset)))] px-2.5 py-1.5 text-sm text-foreground outline-none select-none focus-visible:outline-hidden in-data-highlighting:text-muted-foreground data-[confirming]:bg-transparent data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-muted forced-colors:data-[selected=true]:outline-2 forced-colors:data-[selected=true]:-outline-offset-2 forced-colors:data-[selected=true]:outline-solid pointer-coarse:min-h-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[selected=true]:[&_svg:not([class*='text-'])]:text-foreground [&:is(a)]:cursor-pointer",
        className
      )}
      {...props}
    >
      {link ? React.cloneElement(link, undefined, content) : content}
    </CommandPrimitive.Item>
  )
}

type CommandShortcutProps = React.ComponentProps<"kbd"> & {
  hotkey?: string
}

function CommandShortcut({
  className,
  dir = "ltr",
  hotkey,
  children,
  ...props
}: CommandShortcutProps) {
  const apple = useIsApple()

  return (
    <kbd
      data-slot="command-shortcut"
      dir={dir}
      className={cn(
        "ms-auto shrink-0 font-sans text-xs tracking-widest text-muted-foreground [unicode-bidi:isolate]",
        className
      )}
      {...props}
    >
      {children ?? (hotkey ? formatHotkey(hotkey, apple) : null)}
    </kbd>
  )
}

function CommandKey({ children }: { children: React.ReactNode }) {
  return (
    <kbd className="inline-flex h-5 min-w-5 items-center justify-center rounded-[calc(var(--radius-sm)*0.7)] bg-muted px-1 font-sans text-[0.6875rem] font-medium text-muted-foreground">
      {children}
    </kbd>
  )
}

function CommandFooter({
  className,
  children,
  ...props
}: React.ComponentProps<"div">) {
  const context = React.useContext(CommandContext)
  const nested = (context?.pages.length ?? 0) > 0

  return (
    <div
      data-slot="command-footer"
      className={cn(
        "flex shrink-0 items-center gap-4 border-t px-3.5 py-2 text-xs text-muted-foreground select-none pointer-coarse:hidden",
        className
      )}
      {...props}
    >
      {children ?? (
        <>
          <span className="inline-flex items-center gap-1.5">
            <CommandKey>↑</CommandKey>
            <CommandKey>↓</CommandKey>
            Navigate
          </span>
          <span className="inline-flex items-center gap-1.5">
            <CommandKey>↵</CommandKey>
            Open
          </span>
          {nested ? (
            <span className="inline-flex items-center gap-1.5">
              <CommandKey>⌫</CommandKey>
              Back
            </span>
          ) : null}
          <span className="ms-auto inline-flex items-center gap-1.5">
            <CommandKey>esc</CommandKey>
            {nested ? "Back" : "Close"}
          </span>
        </>
      )}
    </div>
  )
}

type CommandDialogProps = DialogPrimitive.Root.Props & {
  title?: string
  description?: string
  className?: string
  showCloseButton?: boolean
  preserveSearch?: boolean
  children?: React.ReactNode
}

function CommandDialog({
  title = "Command menu",
  description = "Search for a command to run.",
  className,
  showCloseButton = false,
  preserveSearch = false,
  children,
  open,
  defaultOpen = false,
  onOpenChange,
  ...props
}: CommandDialogProps) {
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
  const isOpen = open ?? uncontrolledOpen
  const popupRef = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => {
    if (!isOpen || !preserveSearch) {
      return
    }
    const frame = requestAnimationFrame(() => {
      popupRef.current
        ?.querySelector<HTMLInputElement>("[data-slot=command-input]")
        ?.select()
    })
    return () => cancelAnimationFrame(frame)
  }, [isOpen, preserveSearch])

  return (
    <DialogPrimitive.Root
      open={isOpen}
      onOpenChange={(next, details) => {
        if (
          !next &&
          details.reason === "escape-key" &&
          details.event.defaultPrevented
        ) {
          details.cancel()
          return
        }
        onOpenChange?.(next, details)
        if (!details.isCanceled && open === undefined) {
          setUncontrolledOpen(next)
        }
      }}
      {...props}
    >
      <DialogPrimitive.Portal keepMounted={preserveSearch}>
        <DialogPrimitive.Backdrop
          data-slot="command-dialog-overlay"
          className="fixed inset-0 z-50 min-h-dvh bg-black/40 supports-backdrop-filter:backdrop-blur-xs dark:bg-black/60"
        />
        <DialogPrimitive.Popup
          ref={popupRef}
          data-slot="command-dialog"
          className={cn(
            "fixed inset-x-4 top-[max(env(safe-area-inset-top),1rem)] z-50 mx-auto flex max-w-lg flex-col overflow-hidden rounded-(--command-radius) bg-popover text-popover-foreground shadow-2xl ring-(length:--hairline) ring-foreground/10 outline-none [--command-radius:var(--radius-xl)] focus-visible:outline-hidden sm:top-[12dvh] forced-colors:border",
            className
          )}
        >
          <DialogPrimitive.Title className="sr-only">
            {title}
          </DialogPrimitive.Title>
          <DialogPrimitive.Description className="sr-only">
            {description}
          </DialogPrimitive.Description>
          {children}
          {showCloseButton ? (
            <DialogPrimitive.Close
              data-slot="command-dialog-close"
              render={
                <Button
                  variant="ghost"
                  size="icon-sm"
                  className="absolute end-2 top-2"
                />
              }
            >
              <IconX />
              <span className="sr-only">Close</span>
            </DialogPrimitive.Close>
          ) : null}
        </DialogPrimitive.Popup>
      </DialogPrimitive.Portal>
    </DialogPrimitive.Root>
  )
}

function useCommandHotkey(
  hotkey: string,
  callback: (event: KeyboardEvent) => void,
  { enabled = true }: { enabled?: boolean } = {}
) {
  const callbackRef = React.useRef(callback)

  React.useLayoutEffect(() => {
    callbackRef.current = callback
  })

  React.useEffect(() => {
    if (!enabled) {
      return
    }
    const bare = !/(^|\+)(mod|meta|ctrl|alt)\+/i.test(hotkey)

    const onKeyDown = (event: KeyboardEvent) => {
      if (
        event.defaultPrevented ||
        event.repeat ||
        event.isComposing ||
        !matchesHotkey(event, hotkey) ||
        (bare && isEditable(event.target))
      ) {
        return
      }
      event.preventDefault()
      callbackRef.current(event)
    }

    document.addEventListener("keydown", onKeyDown)
    return () => document.removeEventListener("keydown", onKeyDown)
  }, [enabled, hotkey])
}

function useHotkeyLabel(hotkey: string) {
  return formatHotkey(hotkey, useIsApple())
}

export {
  Command,
  CommandDialog,
  CommandInput,
  CommandList,
  CommandEmpty,
  CommandLoading,
  CommandGroup,
  CommandItem,
  CommandShortcut,
  CommandSeparator,
  CommandPage,
  CommandFooter,
  useCommandHotkey,
  useCommandLoading,
  useCommandPages,
  useCommandState,
  useHotkeyLabel,
}
```

```tsx title="components/ui/button.tsx"
"use client"

import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Spinner } from "@/components/ui/spinner"
import {
  useButtonFeedback,
  type ButtonFeedbackOptions,
  type ButtonStatus,
} from "@/hooks/use-button-feedback"

const buttonVariants = cva(
  "group/button relative inline-flex items-center justify-center rounded-md bg-clip-padding text-sm font-medium whitespace-nowrap inset-ring-(length:--hairline) inset-ring-transparent transition-[color,background-color,box-shadow,opacity,translate,scale] duration-200 ease-out-quint outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:duration-100 active:not-aria-[haspopup]:not-aria-busy:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive motion-safe:active:not-aria-[haspopup]:not-aria-busy:scale-[0.97] dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 forced-colors:border [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "bg-background inset-ring-border hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30 dark:inset-ring-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 forced-colors:border-0",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/70",
        link: "text-primary underline-offset-4 hover:underline forced-colors:border-0",
        success:
          "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/70 focus-visible:inset-ring-success/40",
        error:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 motion-safe:animate-button-shake",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        icon: "size-9 shrink-0",
        "icon-xs":
          "size-6 shrink-0 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:in-data-[slot=button-group]:after:hidden [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 shrink-0 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-1.5 pointer-coarse:in-data-[slot=button-group]:after:hidden",
        "icon-lg": "size-10 shrink-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type ButtonVariant = Exclude<
  NonNullable<VariantProps<typeof buttonVariants>["variant"]>,
  "success" | "error"
>

type ButtonClickEvent = Parameters<
  NonNullable<ButtonPrimitive.Props["onClick"]>
>[0]

type ButtonProps = Omit<ButtonPrimitive.Props, "onClick"> &
  Omit<VariantProps<typeof buttonVariants>, "variant"> &
  ButtonFeedbackOptions & {
    variant?: ButtonVariant | null
    onClick?: (event: ButtonClickEvent) => unknown
    feedback?: boolean
    loading?: boolean
    status?: ButtonStatus
    loadingLabel?: React.ReactNode
    successLabel?: React.ReactNode
    errorLabel?: React.ReactNode | ((error: unknown) => React.ReactNode)
  }

const stateTransitionTime = 220
const statuses: ButtonStatus[] = ["idle", "loading", "success", "error"]
const quietVariants: ButtonVariant[] = ["ghost", "link"]
const quietStatusClasses: Partial<Record<ButtonStatus, string>> = {
  success: "text-success hover:text-success",
  error:
    "text-destructive hover:text-destructive motion-safe:animate-button-shake",
}

function isThenable(value: unknown): value is PromiseLike<unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as PromiseLike<unknown>).then === "function"
  )
}

function useStatusTransition(status: ButtonStatus) {
  const [shown, setShown] = React.useState(status)
  const [leaving, setLeaving] = React.useState<ButtonStatus | null>(null)
  const [transitionKey, setTransitionKey] = React.useState(0)

  if (status !== shown) {
    setLeaving(shown)
    setShown(status)
    setTransitionKey((key) => key + 1)
  }

  React.useEffect(() => {
    if (leaving === null) {
      return
    }
    const timer = setTimeout(() => setLeaving(null), stateTransitionTime)
    return () => clearTimeout(timer)
  }, [leaving, transitionKey])

  return { leaving, transitionKey }
}

function useLayerWidth(status: ButtonStatus, active: boolean) {
  const contentRef = React.useRef<HTMLSpanElement>(null)
  const layersRef = React.useRef(new Map<ButtonStatus, HTMLElement>())
  const observerRef = React.useRef<ResizeObserver | null>(null)
  const statusRef = React.useRef(status)

  const applyWidth = React.useCallback(() => {
    const content = contentRef.current
    const layer = layersRef.current.get(statusRef.current)
    if (!content || !layer) {
      return
    }
    const natural = parseFloat(getComputedStyle(layer).width)
    if (!Number.isFinite(natural)) {
      return
    }
    content.style.width = `${natural}px`
    if (!content.hasAttribute("data-measured")) {
      requestAnimationFrame(() => content.setAttribute("data-measured", ""))
    }
  }, [])

  React.useLayoutEffect(() => {
    statusRef.current = status
    if (active) {
      applyWidth()
    }
  }, [active, applyWidth, status])

  React.useLayoutEffect(() => {
    if (!active || typeof ResizeObserver === "undefined") {
      return
    }
    const observer = new ResizeObserver(applyWidth)
    observerRef.current = observer
    layersRef.current.forEach((layer) => observer.observe(layer))
    document.fonts?.addEventListener?.("loadingdone", applyWidth)
    return () => {
      observer.disconnect()
      observerRef.current = null
      document.fonts?.removeEventListener?.("loadingdone", applyWidth)
    }
  }, [active, applyWidth])

  const layerRef = React.useCallback((node: HTMLElement | null) => {
    if (!node) {
      return
    }
    const layer = node.dataset.layer as ButtonStatus
    layersRef.current.set(layer, node)
    observerRef.current?.observe(node)
    return () => {
      observerRef.current?.unobserve(node)
      if (layersRef.current.get(layer) === node) {
        layersRef.current.delete(layer)
      }
    }
  }, [])

  return { contentRef, layerRef }
}

function composeHandlers<Event>(
  first: ((event: Event) => void) | undefined,
  second: (event: Event) => void
) {
  return (event: Event) => {
    first?.(event)
    second(event)
  }
}

function Button({
  className,
  variant = "default",
  size = "default",
  children,
  onClick,
  disabled,
  focusableWhenDisabled,
  feedback,
  loading,
  status: statusProp,
  onStatusChange,
  onError,
  resetAfter,
  loadingLabel,
  successLabel = "Done",
  errorLabel = "Failed",
  onPointerEnter,
  onPointerLeave,
  onFocus,
  onBlur,
  ...props
}: ButtonProps) {
  const internal = useButtonFeedback({ resetAfter, onStatusChange, onError })
  const status = statusProp ?? (loading ? "loading" : internal.status)
  const requested =
    Boolean(feedback) || loading !== undefined || statusProp !== undefined
  const [enabled, setEnabled] = React.useState(requested)
  if (requested && !enabled) {
    setEnabled(true)
  }

  const { leaving, transitionKey } = useStatusTransition(status)
  const iconOnly = size?.startsWith("icon") ?? false
  const widthStatus =
    status === "loading" && loadingLabel === undefined ? "idle" : status
  const { contentRef, layerRef } = useLayerWidth(
    widthStatus,
    enabled && !iconOnly
  )
  const isLoading = status === "loading"
  const quiet = quietVariants.includes(variant ?? "default")
  const resolvedVariant =
    !quiet && status === "success"
      ? "success"
      : !quiet && status === "error"
        ? "error"
        : variant
  const resolvedErrorLabel =
    typeof errorLabel !== "function"
      ? errorLabel
      : internal.error !== undefined
        ? errorLabel(internal.error)
        : "Failed"

  const handleClick = (event: ButtonClickEvent) => {
    if (feedback && internal.isPending()) {
      return
    }
    const result = onClick?.(event)
    if (feedback && isThenable(result)) {
      internal.track(result)
    }
  }

  const layerContent = (layer: ButtonStatus, animated: boolean) => {
    if (layer === "idle") {
      return children
    }
    if (layer === "loading") {
      return (
        <>
          <Spinner aria-hidden size={null} animated={animated} />
          {iconOnly ? null : loadingLabel}
        </>
      )
    }
    if (layer === "success") {
      return (
        <>
          <IconCircleCheck
            className={cn(
              animated &&
                "motion-safe:[&>path:first-child]:animate-button-draw-circle motion-safe:[&>path:first-child]:[stroke-dasharray:57] motion-safe:[&>path:first-child]:[stroke-dashoffset:57] motion-safe:[&>path:last-child]:animate-button-draw-check motion-safe:[&>path:last-child]:[stroke-dasharray:9] motion-safe:[&>path:last-child]:[stroke-dashoffset:9]"
            )}
          />
          {iconOnly ? null : successLabel}
        </>
      )
    }
    return (
      <>
        <IconAlertCircle />
        {iconOnly ? null : resolvedErrorLabel}
      </>
    )
  }

  const button = (
    <ButtonPrimitive
      data-slot="button"
      data-status={enabled ? status : undefined}
      aria-busy={isLoading || undefined}
      disabled={disabled || isLoading}
      focusableWhenDisabled={focusableWhenDisabled ?? (isLoading || undefined)}
      className={cn(
        buttonVariants({ variant: resolvedVariant, size }),
        quiet && quietStatusClasses[status],
        className
      )}
      onClick={handleClick}
      onPointerEnter={composeHandlers(
        onPointerEnter,
        internal.buttonProps.onPointerEnter
      )}
      onPointerLeave={composeHandlers(
        onPointerLeave,
        internal.buttonProps.onPointerLeave
      )}
      onFocus={composeHandlers(onFocus, internal.buttonProps.onFocus)}
      onBlur={composeHandlers(onBlur, internal.buttonProps.onBlur)}
      {...props}
    >
      {enabled ? (
        <span
          ref={contentRef}
          data-slot="button-content"
          className="relative inline-grid grid-cols-[minmax(0,1fr)] place-items-center gap-[inherit] overflow-x-clip *:col-start-1 *:row-start-1 motion-safe:data-measured:transition-[width] motion-safe:data-measured:duration-220 motion-safe:data-measured:ease-[cubic-bezier(0.2,0,0,1)]"
        >
          {statuses.map((layer) => {
            const active = layer === status
            const exiting = layer === leaving
            const namesButton =
              layer === "idle" && isLoading && loadingLabel === undefined

            return (
              <span
                key={active ? `${layer}-${transitionKey}` : layer}
                ref={layerRef}
                data-slot="button-layer"
                data-layer={layer}
                aria-hidden={active || namesButton ? undefined : true}
                className={cn(
                  "inline-flex items-center justify-center gap-[inherit] backface-hidden",
                  !active &&
                    !exiting &&
                    "pointer-events-none absolute opacity-0",
                  active &&
                    transitionKey > 0 &&
                    "origin-bottom motion-safe:animate-button-state-in motion-reduce:animate-in motion-reduce:animation-duration-150 motion-reduce:fade-in-0",
                  exiting &&
                    "pointer-events-none origin-top opacity-0 motion-safe:animate-button-state-out"
                )}
              >
                {layerContent(layer, active || exiting)}
              </span>
            )
          })}
        </span>
      ) : (
        children
      )}
    </ButtonPrimitive>
  )

  if (!enabled) {
    return button
  }

  const announcement =
    status === "loading"
      ? typeof loadingLabel === "string"
        ? loadingLabel
        : "Loading"
      : status === "success"
        ? typeof successLabel === "string"
          ? successLabel
          : "Done"
        : status === "error"
          ? typeof resolvedErrorLabel === "string"
            ? resolvedErrorLabel
            : "Failed"
          : ""

  return (
    <>
      {button}
      <span data-slot="button-status" role="status" className="sr-only">
        {announcement}
      </span>
    </>
  )
}

export { Button, buttonVariants, useButtonFeedback }
export type { ButtonProps, ButtonStatus, ButtonFeedbackOptions }
```

```ts title="lib/motion.ts"
import * as React from "react"

const easeOut = "cubic-bezier(0.23, 1, 0.32, 1)"
const easeInOut = "cubic-bezier(0.77, 0, 0.175, 1)"
const easeSpring =
  "linear(0, 0.015 2%, 0.0532 4%, 0.1065 6%, 0.1686 8%, 0.2351 10%, 0.3363 13%, 0.4332 16%, 0.5495 20%, 0.648 24%, 0.7287 28%, 0.807 33%, 0.8646 38%, 0.9128 44%, 0.9447 50%, 0.968 57%, 0.9834 65%, 0.9924 74%, 0.9975 85%, 1)"

const duration = {
  press: 100,
  release: 200,
  hover: 150,
  enter: 200,
  exit: 150,
  morph: 300,
} as const

function prefersReducedMotion() {
  return (
    typeof window === "undefined" ||
    typeof window.matchMedia !== "function" ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  )
}

type SizeAxis = "width" | "height"

type SizeMorphOptions = {
  axis: SizeAxis
  enabled?: boolean
  duration?: number
  easing?: string
}

function readSize(element: HTMLElement, axis: SizeAxis) {
  const value = parseFloat(getComputedStyle(element)[axis])
  if (Number.isFinite(value)) {
    return value
  }
  const rect = element.getBoundingClientRect()
  return axis === "width" ? rect.width : rect.height
}

function attachSizeMorph(
  element: HTMLElement,
  axis: SizeAxis,
  time: number,
  easing: string
) {
  if (
    typeof MutationObserver === "undefined" ||
    typeof element.animate !== "function"
  ) {
    return undefined
  }

  let settled = readSize(element, axis)
  let animation: Animation | null = null

  const morph = () => {
    const from = animation ? readSize(element, axis) : settled
    animation?.cancel()
    animation = null
    const to = readSize(element, axis)
    settled = to

    if (
      Math.abs(from - to) < 0.5 ||
      prefersReducedMotion() ||
      !element.isConnected ||
      element.getClientRects().length === 0
    ) {
      element.removeAttribute("data-morphing")
      return
    }

    element.setAttribute("data-morphing", "")
    const running = element.animate(
      [{ [axis]: `${from}px` }, { [axis]: `${to}px` }],
      { duration: time, easing }
    )
    animation = running
    running.onfinish = () => {
      if (animation === running) {
        animation = null
        element.removeAttribute("data-morphing")
        settled = readSize(element, axis)
      }
    }
  }

  const mutations = new MutationObserver(morph)
  mutations.observe(element, {
    childList: true,
    subtree: true,
    characterData: true,
  })

  const resize =
    typeof ResizeObserver === "undefined"
      ? null
      : new ResizeObserver(() => {
          if (!animation) {
            settled = readSize(element, axis)
          }
        })
  resize?.observe(element)

  return () => {
    mutations.disconnect()
    resize?.disconnect()
    animation?.cancel()
    element.removeAttribute("data-morphing")
  }
}

function useSizeMorph<T extends HTMLElement>({
  axis,
  enabled = true,
  duration: time = duration.morph,
  easing = easeOut,
}: SizeMorphOptions): React.RefCallback<T> {
  return React.useCallback(
    (element: T | null) => {
      if (!element || !enabled) {
        return undefined
      }
      return attachSizeMorph(element, axis, time, easing)
    },
    [axis, enabled, time, easing]
  )
}

function useSlidingHighlight(
  barRef: React.RefObject<HTMLElement | null>,
  highlightRef: React.RefObject<HTMLElement | null>,
  selector: string,
  attribute = "data-popup-open"
) {
  React.useLayoutEffect(() => {
    const bar = barRef.current
    const highlight = highlightRef.current
    if (!bar || !highlight) {
      return
    }

    let current: HTMLElement | null = null

    const place = (trigger: HTMLElement, instant: boolean) => {
      if (instant || prefersReducedMotion()) {
        highlight.setAttribute("data-instant", "")
      } else {
        highlight.removeAttribute("data-instant")
      }
      const frame = bar.getBoundingClientRect()
      const box = trigger.getBoundingClientRect()
      const scale = bar.offsetWidth > 0 ? frame.width / bar.offsetWidth : 1
      highlight.style.left = "0px"
      highlight.style.width = `${box.width / scale}px`
      highlight.style.height = `${box.height / scale}px`
      highlight.style.transform = `translate(${(box.left - frame.left) / scale - bar.clientLeft}px, ${(box.top - frame.top) / scale - bar.clientTop}px)`
    }

    const sync = () => {
      const trigger = bar.querySelector<HTMLElement>(selector)
      if (trigger === current) {
        if (trigger) {
          place(trigger, true)
        }
        return
      }
      const appearing = current === null
      current = trigger
      if (!trigger) {
        highlight.removeAttribute("data-visible")
        return
      }
      place(trigger, appearing)
      if (appearing) {
        void highlight.offsetWidth
      }
      highlight.setAttribute("data-visible", "")
    }

    sync()
    const mutations = new MutationObserver(sync)
    mutations.observe(bar, {
      subtree: true,
      childList: true,
      attributes: true,
      attributeFilter: [attribute],
    })
    const resize =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            if (current) {
              place(current, true)
            }
          })
    resize?.observe(bar)
    const onScroll = () => {
      if (current) {
        place(current, true)
      }
    }
    bar.addEventListener("scroll", onScroll, { capture: true, passive: true })

    return () => {
      mutations.disconnect()
      resize?.disconnect()
      bar.removeEventListener("scroll", onScroll, { capture: true })
    }
  }, [barRef, highlightRef, selector, attribute])
}

export {
  duration,
  easeInOut,
  easeOut,
  easeSpring,
  prefersReducedMotion,
  useSizeMorph,
  useSlidingHighlight,
}
export type { SizeMorphOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
```

```tsx
<Command>
  <CommandInput placeholder="Type a command or search…" />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading="Suggestions">
      <CommandItem onSelect={() => openCalendar()}>Calendar</CommandItem>
      <CommandItem shortcut="mod+p">Profile</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>
```

Hotkeys use `mod` for ⌘ on Apple devices and Ctrl everywhere else. Labels are formatted per platform for you.

## Composition

```text
Command
├── CommandInput
├── CommandList
│   ├── CommandEmpty
│   ├── CommandLoading
│   ├── CommandGroup
│   │   └── CommandItem
│   │       └── CommandShortcut
│   ├── CommandSeparator
│   └── CommandPage
│       └── CommandGroup
└── CommandFooter

CommandDialog
└── Command
```

## Examples

### Basic

Typing filters and ranks items as you go. Groups with no matches disappear, and the list height animates to fit what is left.

```tsx title="components/examples/command/basic.tsx"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandBasic() {
  return (
    <Command className="w-full max-w-sm">
      <CommandInput placeholder="Type a command or search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>Calendar</CommandItem>
          <CommandItem>Search Emoji</CommandItem>
          <CommandItem>Calculator</CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>Profile</CommandItem>
          <CommandItem>Billing</CommandItem>
          <CommandItem>Settings</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

### Dialog

Put a `<Command />` inside `<CommandDialog />` and toggle it with `useCommandHotkey`. Press ⌘K or Ctrl K. Item shortcuts run while it is open, matches are highlighted, and `preserveSearch` keeps the query and selection for the next time it opens.

```tsx title="components/examples/command/dialog.tsx"
"use client"

import * as React from "react"
import {
  IconCalculator,
  IconCalendar,
  IconCheck,
  IconCreditCard,
  IconExternalLink,
  IconMoodSmile,
  IconPalette,
  IconPoint,
  IconSettings,
  IconUser,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandFooter,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPage,
  CommandSeparator,
  CommandShortcut,
  useCommandHotkey,
} from "@/components/ui/command"

export function CommandDialogDemo() {
  const [open, setOpen] = React.useState(false)
  const [last, setLast] = React.useState<string>()
  const [theme, setTheme] = React.useState("System")

  useCommandHotkey("mod+k", () => setOpen((value) => !value))

  const run = (action: string) => {
    setLast(action)
    setOpen(false)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open palette
        <CommandShortcut hotkey="mod+k" />
      </Button>
      <p className="text-sm text-muted-foreground">
        {last ? `Ran “${last}”.` : "Nothing run yet."} Theme: {theme}.
      </p>
      <CommandDialog open={open} onOpenChange={setOpen} preserveSearch>
        <Command highlight>
          <CommandInput placeholder="Type a command or search…" />
          <CommandList>
            <CommandEmpty>
              {(search) => `No results for “${search}”.`}
            </CommandEmpty>
            <CommandGroup heading="Suggestions">
              <CommandItem
                shortcut="mod+shift+c"
                onSelect={() => run("Calendar")}
              >
                <IconCalendar />
                Calendar
              </CommandItem>
              <CommandItem onSelect={() => run("Search Emoji")}>
                <IconMoodSmile />
                Search Emoji
              </CommandItem>
              <CommandItem disabled>
                <IconCalculator />
                Calculator
              </CommandItem>
              <CommandItem page="theme" pageTitle="Theme">
                <IconPalette />
                Change theme…
              </CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Settings">
              <CommandItem shortcut="mod+p" onSelect={() => run("Profile")}>
                <IconUser />
                Profile
              </CommandItem>
              <CommandItem shortcut="mod+b" onSelect={() => run("Billing")}>
                <IconCreditCard />
                Billing
              </CommandItem>
              <CommandItem
                shortcut="mod+,"
                keywords={["preferences", "options"]}
                onSelect={() => run("Settings")}
              >
                <IconSettings />
                Settings
              </CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Links">
              <CommandItem href="/docs" onSelect={() => setOpen(false)}>
                <IconExternalLink />
                All components
              </CommandItem>
            </CommandGroup>
            <CommandPage id="theme">
              <CommandGroup heading="Theme">
                {["Light", "Dark", "System"].map((option) => (
                  <CommandItem
                    key={option}
                    onSelect={() => {
                      setTheme(option)
                      run(`Theme: ${option}`)
                    }}
                  >
                    {option === theme ? <IconCheck /> : <IconPoint />}
                    {option}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandPage>
          </CommandList>
          <CommandFooter />
        </Command>
      </CommandDialog>
    </div>
  )
}
```

### Pages

An item with `page` opens the matching `<CommandPage />`. The page title appears as a chip in the input, the list slides in from the side, and Backspace on an empty search or Escape goes back.

```tsx title="components/examples/command/pages.tsx"
"use client"

import * as React from "react"
import {
  IconBrandGithub,
  IconFolder,
  IconFolderPlus,
  IconUsers,
} from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPage,
} from "@/components/ui/command"

const projects = ["hextaui", "marketing-site", "design-tokens"]
const members = ["Ada Lovelace", "Grace Hopper", "Alan Turing"]

export function CommandPages() {
  const [last, setLast] = React.useState<string>()

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <Command>
        <CommandInput placeholder="Search…" />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandGroup heading="Workspace">
            <CommandItem page="projects" pageTitle="Projects">
              <IconFolder />
              Projects
            </CommandItem>
            <CommandItem page="members" pageTitle="Members">
              <IconUsers />
              Members
            </CommandItem>
            <CommandItem onSelect={() => setLast("New project")}>
              <IconFolderPlus />
              New project
            </CommandItem>
          </CommandGroup>
          <CommandPage id="projects">
            <CommandGroup heading="Projects">
              {projects.map((project) => (
                <CommandItem key={project} onSelect={setLast}>
                  <IconBrandGithub />
                  {project}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandPage>
          <CommandPage id="members">
            <CommandGroup heading="Members">
              {members.map((member) => (
                <CommandItem key={member} onSelect={setLast}>
                  <IconUsers />
                  {member}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandPage>
        </CommandList>
      </Command>
      <p className="text-sm text-muted-foreground">
        {last ? `Selected “${last}”.` : "Nothing selected yet."}
      </p>
    </div>
  )
}
```

### Scrollable

Long lists scroll inside a capped height. The selected item is always kept in view while you move with the keyboard.

```tsx title="components/examples/command/scrollable.tsx"
import { IconFileText } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"

const documents = Array.from({ length: 60 }, (_, index) => ({
  id: index + 1,
  title: `Document ${String(index + 1).padStart(2, "0")}`,
}))

export function CommandScrollable() {
  return (
    <Command className="w-full max-w-sm">
      <CommandInput placeholder="Search 60 documents…" />
      <CommandList>
        <CommandEmpty>No documents match.</CommandEmpty>
        <CommandGroup heading="Documents">
          {documents.map((doc) => (
            <CommandItem key={doc.id}>
              <IconFileText />
              {doc.title}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

### Async results

Set `shouldFilter={false}` and render the results you fetch. `<CommandLoading />` waits 150 ms before it appears and then stays at least 300 ms, so fast responses never flash a spinner. Try both latencies.

```tsx title="components/examples/command/async.tsx"
"use client"

import * as React from "react"
import { IconUser } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandLoading,
  useCommandLoading,
} from "@/components/ui/command"

const people = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Katherine Johnson",
  "Linus Torvalds",
  "Margaret Hamilton",
  "Tim Berners-Lee",
]

export function CommandAsync() {
  const [query, setQuery] = React.useState("")
  const [results, setResults] = React.useState(people)
  const [loading, setLoading] = React.useState(false)
  const [latency, setLatency] = React.useState(700)
  const pending = useCommandLoading(loading)
  const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timerRef.current), [])

  const search = (nextQuery: string, nextLatency: number) => {
    clearTimeout(timerRef.current)
    setLoading(true)
    timerRef.current = setTimeout(() => {
      setResults(
        people.filter((person) =>
          person.toLowerCase().includes(nextQuery.toLowerCase())
        )
      )
      setLoading(false)
    }, nextLatency)
  }

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex gap-2">
        {[80, 700].map((ms) => (
          <Button
            key={ms}
            size="sm"
            variant={latency === ms ? "secondary" : "outline"}
            onClick={() => {
              setLatency(ms)
              search(query, ms)
            }}
          >
            {ms} ms
          </Button>
        ))}
      </div>
      <Command shouldFilter={false} highlight>
        <CommandInput
          placeholder="Search people…"
          value={query}
          onValueChange={(next) => {
            setQuery(next)
            search(next, latency)
          }}
        />
        <CommandList>
          <CommandLoading loading={loading}>Searching…</CommandLoading>
          <CommandEmpty>
            {(value) => `No people match “${value}”.`}
          </CommandEmpty>
          {pending ? null : (
            <CommandGroup heading="People">
              {results.map((person) => (
                <CommandItem key={person}>
                  <IconUser />
                  {person}
                </CommandItem>
              ))}
            </CommandGroup>
          )}
        </CommandList>
      </Command>
    </div>
  )
}
```

### Long content

Headings wrap, long names truncate or wrap as you choose, and shortcuts never get pushed out.

```tsx title="components/examples/command/long-content.tsx"
import { IconFileText } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"

export function CommandLongContent() {
  return (
    <Command className="w-full max-w-72">
      <CommandInput placeholder="Search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="A group heading that is long enough to wrap onto two lines">
          <CommandItem>
            <IconFileText />
            <span className="min-w-0 truncate">
              quarterly-planning-final-final-v2-reviewed-by-legal.pdf
            </span>
            <CommandShortcut>⌘⇧O</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <IconFileText />
            <span className="min-w-0 wrap-anywhere">
              averyveryverylongunbrokenfilenamethatshouldwrapinsteadofescaping.txt
            </span>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

### Right to left

Icons, shortcuts, the page chip and the page slide all follow the reading direction.

```tsx title="components/examples/command/rtl.tsx"
import { IconCalendar, IconSettings } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"

export function CommandRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <Command dir="rtl">
        <CommandInput placeholder="ابحث عن أمر…" />
        <CommandList>
          <CommandEmpty>لا توجد نتائج.</CommandEmpty>
          <CommandGroup heading="اقتراحات">
            <CommandItem>
              <IconCalendar />
              التقويم
              <CommandShortcut>⌘T</CommandShortcut>
            </CommandItem>
            <CommandItem>
              <IconSettings />
              الإعدادات
              <CommandShortcut>⌘S</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </Command>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `↓` | Selects the next item. |
| `↑` | Selects the previous item. |
| `Alt` `↓` | Jumps to the first item of the next group. |
| `Alt` `↑` | Jumps to the first item of the previous group. |
| `Home` | Selects the first item. |
| `End` | Selects the last item. |
| `Ctrl` `N` | Selects the next item. Ctrl J works too. Turn off with vimBindings. |
| `Ctrl` `P` | Selects the previous item. Ctrl K works too. Turn off with vimBindings. |
| `Enter` | Runs the selected item. On a link item, ⌘ Enter or Ctrl Enter opens it in a new tab. |
| `Esc` | Clears the search first, then goes back one page, then closes the dialog. |
| `Backspace` | Goes back one page when the search is empty. |
| `⌘` `P` | Any item shortcut runs its item while focus is inside the command menu. |

## Accessibility

- The input is a combobox that points at the selected item, so screen readers announce each item as you move.
- A polite live region announces the number of results shortly after you stop typing, and announces the page title when you open or leave a page. Change the wording with `formatResults` and `rootTitle`.
- `<CommandDialog />` has a hidden title and description, traps focus while open and returns it to the trigger when it closes.
- Item shortcuts are exposed with `aria-keyshortcuts`.
- With reduced motion on, items run without the confirm blink and pages fade instead of sliding.

## API reference

Built on `cmdk`, with `<CommandDialog />` on the Base UI dialog. Parts accept the props of the cmdk part they wrap.

### Command

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `"Command menu"` | Accessible name of the menu. |
| `highlight` | `boolean` | `false` | Highlights the matched letters in each item and dims the rest. |
| `shouldFilter` | `boolean` | `true` | Set to false to filter and sort the items yourself, for example when results come from a server. |
| `filter` | `(value: string, search: string, keywords?: string[]) => number` | – | Returns a score from 0 (hidden) to 1 (best match). |
| `value` | `string` | – | The selected item’s value. |
| `defaultValue` | `string` | – |  |
| `onValueChange` | `(value: string) => void` | – |  |
| `loop` | `boolean` | `false` | Wrap around at the ends of the list. |
| `vimBindings` | `boolean` | `true` | Ctrl N, J, P and K navigation. |
| `disablePointerSelection` | `boolean` | `false` |  |
| `formatResults` | `(count: number) => string` | `"3 results"` | Text announced to screen readers after typing. |
| `rootTitle` | `string` | `"All commands"` | Announced when you leave the last page and return to the root. |

| Attribute | Description |
| --- | --- |
| `data-slot="command"` | Target the root in CSS. |
| `data-highlighting` | Present while highlight is on and the search is not empty. |
| `--command-radius` | Outer radius. Items derive a concentric radius from it. |
| `--command-inset` | Padding between the list edge and its items. |

### CommandDialog

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean, details) => void` | – |  |
| `preserveSearch` | `boolean` | `false` | Keep the dialog mounted so the query, page and selection survive closing. The query is selected when it reopens. |
| `title` | `string` | `"Command menu"` | Visually hidden dialog title. |
| `description` | `string` | `"Search for a command to run."` | Visually hidden dialog description. |
| `showCloseButton` | `boolean` | `false` |  |
| `className` | `string` | – | Applied to the dialog popup. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-dialog"` | The dialog popup. |
| `data-slot="command-dialog-overlay"` | The backdrop. |
| `data-open` | Present on the popup while it is open. |

### CommandInput

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | Controlled search text. |
| `onValueChange` | `(search: string) => void` | – |  |
| `placeholder` | `string` | – |  |
| `clearLabel` | `string` | `"Clear search"` | Accessible name of the clear button. |
| `backLabel` | `(title: string) => string` | `` (title) => `Back from ${title}` `` | Accessible name of the page chip. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-input"` | The input. |
| `data-slot="command-input-wrapper"` | The row holding the icon, input and clear button. |
| `data-slot="command-clear"` | The clear button, shown once you type. |
| `data-slot="command-page-chip"` | The back chip shown on a page. |

### CommandList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | – | Accessible name of the list. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-list"` | The list. |
| `data-settled` | Present once the list has measured itself. The height transition only runs while it is set. |
| `--cmdk-list-height` | Height of the visible items, used to animate the list. |

### CommandEmpty

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode \| (search: string) => ReactNode` | – | Use the function form to echo the query. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-empty"` | Hidden while a CommandLoading is in the list. |

### CommandLoading

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `true` |  |
| `delay` | `number` | `150` | Milliseconds to wait before the spinner shows. |
| `minDuration` | `number` | `300` | Minimum milliseconds the spinner stays once shown. |
| `label` | `string` | – | Accessible label. Defaults to string children. |
| `progress` | `number` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="command-loading"` | The loading row. |
| `data-pending` | Present during the delay, while the row is announced but not yet visible. |

### CommandGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `heading` | `ReactNode` | – |  |
| `value` | `string` | – | Required when there is no heading. |
| `forceMount` | `boolean` | `false` | Keep the group visible while filtering. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-group"` | The group. |
| `[cmdk-group-heading]` | The heading element. |

### CommandItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSelect` | `(value: string) => void` | – | Runs on click, Enter or the item’s shortcut, after the confirm blink. |
| `value` | `string` | – | Used for filtering. Defaults to the item’s text, without the shortcut. |
| `keywords` | `string[]` | – | Extra words that match this item. |
| `disabled` | `boolean` | `false` |  |
| `shortcut` | `string` | – | A hotkey like "mod+shift+c". Shown on the item and runs it while focus is in the menu. |
| `page` | `string` | – | Opens the CommandPage with this id instead of running. |
| `pageTitle` | `string` | – | Title shown in the page chip. Defaults to the value. |
| `href` | `string` | – | Renders the item as a link. Enter follows it, ⌘ or Ctrl Enter opens a new tab. |
| `render` | `ReactElement` | – | A link element to render instead, like Next.js <Link />. |
| `confirm` | `boolean` | `true` | Blink the item briefly before running it, so the choice registers. |
| `forceMount` | `boolean` | `false` | Keep the item visible while filtering. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-item"` | The item. |
| `data-selected="true"` | Present on the selected item. |
| `data-disabled="true"` | Present on disabled items. |
| `data-value` | The value used for filtering. |
| `data-confirming` | Present during the confirm blink. |
| `data-page` | Present on items that open a page. |

### CommandPage

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | – | Matches the page prop of the item that opens it. Its groups and items only render while it is the current page. |

### CommandShortcut

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hotkey` | `string` | – | Formats a hotkey like "mod+k" for the current platform. Children override it. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-shortcut"` | The shortcut label. |

### CommandSeparator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alwaysRender` | `boolean` | `false` | Keep it visible while searching. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-separator"` | The separator. |

### CommandFooter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | – | Defaults to key hints that update on a page. Hidden on touch screens. |

| Attribute | Description |
| --- | --- |
| `data-slot="command-footer"` | The footer. |

### useCommandHotkey

```tsx
const [open, setOpen] = React.useState(false)

useCommandHotkey("mod+k", () => setOpen((value) => !value))
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hotkey` | `string` | – | Listened for on the whole document. Hotkeys without a modifier are ignored while typing in a field. |
| `callback` | `(event: KeyboardEvent) => void` | – |  |
| `options.enabled` | `boolean` | `true` |  |

### useCommandLoading

Returns whether a loading indicator should be visible, with the same delay and minimum duration as `<CommandLoading />`. Use it to hide stale results while a request is in flight.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | – |  |
| `options.delay` | `number` | `150` |  |
| `options.minDuration` | `number` | `300` |  |

### Other hooks

- `useCommandPages()` returns `{ pages, page, push, pop, reset }` to drive pages from your own code.
- `useCommandState(selector)` reads cmdk state, such as the search or the filtered count.
- `useHotkeyLabel(hotkey)` formats a hotkey for the current platform, like ⌘K or Ctrl+K.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
