{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"command-palette","type":"registry:component","title":"Command Palette","description":"Command-K style search overlay that filters as you type, with arrow keys to move the highlight and Enter to select.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/command-palette.tsx","type":"registry:component","target":"@components/motion/command-palette.tsx","content":"\"use client\";\n// easeui.dev/components/motion/command-palette\n\nimport { Search } from \"lucide-react\";\nimport {\n  Children,\n  createContext,\n  Fragment,\n  type InputHTMLAttributes,\n  isValidElement,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CommandItemProps {\n  /** Text matched against the search query. Falls back to string children. */\n  value?: string;\n  /** Extra terms that also match, invisibly, such as synonyms or a keyboard shortcut. */\n  keywords?: string[];\n  onSelect?: () => void;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\nfunction itemValue(props: { value?: string; children?: ReactNode }): string {\n  return props.value ?? (typeof props.children === \"string\" ? props.children : \"\");\n}\n\nfunction isMatch(props: CommandItemProps, query: string): boolean {\n  const trimmed = query.trim().toLowerCase();\n  if (!trimmed) return true;\n  const haystack = [itemValue(props), ...(props.keywords ?? [])].join(\" \").toLowerCase();\n  return haystack.includes(trimmed);\n}\n\n/** Walks a children tree, collecting every enabled item that matches the query, in order. */\nfunction collectItems(children: ReactNode, query: string): CommandItemProps[] {\n  const found: CommandItemProps[] = [];\n  for (const child of Children.toArray(children)) {\n    if (!isValidElement(child)) continue;\n    if (child.type === Fragment || child.type === CommandGroup || child.type === CommandList) {\n      found.push(...collectItems((child.props as { children?: ReactNode }).children, query));\n    } else if (child.type === CommandItem) {\n      const props = child.props as CommandItemProps;\n      if (!props.disabled && isMatch(props, query)) found.push(props);\n    }\n  }\n  return found;\n}\n\ninterface CommandContextValue {\n  query: string;\n  setQuery: (query: string) => void;\n  activeValue: string | undefined;\n  setActiveValue: (value: string) => void;\n  select: (value: string) => void;\n  count: number;\n  listboxId: string;\n}\n\nconst CommandContext = createContext<CommandContextValue | null>(null);\n\nfunction useCommand(part: string) {\n  const ctx = useContext(CommandContext);\n  if (!ctx) throw new Error(`${part} must be used inside <CommandPalette>`);\n  return ctx;\n}\n\nexport interface CommandPaletteProps {\n  /** Whether the palette is open. */\n  open: boolean;\n  /** Called when the palette asks to close, from Escape, the backdrop, or a selection. */\n  onOpenChange: (open: boolean) => void;\n  /** Names the dialog for screen readers. Default \"Command palette\". */\n  label?: string;\n  className?: string;\n  children: ReactNode;\n}\n\n/**\n * A command-K style search overlay on the native dialog element. Typing\n * filters the list, arrow keys move the highlight, and Enter selects it.\n * The dialog handles focus and an inert page behind it on its own.\n */\nexport function CommandPalette({\n  open,\n  onOpenChange,\n  label = \"Command palette\",\n  className,\n  children,\n}: CommandPaletteProps) {\n  const ref = useRef<HTMLDialogElement>(null);\n  const listboxId = useId();\n  const [query, setQuery] = useState(\"\");\n  const [activeValue, setActiveValue] = useState<string>();\n  const [shown, setShown] = useState(false);\n\n  const items = useMemo(() => collectItems(children, query), [children, query]);\n\n  // Reset the highlight only once the active item filters out.\n  useEffect(() => {\n    if (items.some((item) => itemValue(item) === activeValue)) return;\n    setActiveValue(items[0] ? itemValue(items[0]) : undefined);\n  }, [items, activeValue]);\n\n  useEffect(() => {\n    const dialog = ref.current;\n    if (!dialog) return;\n\n    if (open) {\n      setQuery(\"\");\n      if (!dialog.open) dialog.showModal();\n      const frame = requestAnimationFrame(() => setShown(true));\n      return () => cancelAnimationFrame(frame);\n    }\n\n    setShown(false);\n    if (!dialog.open) return;\n    const timeout = setTimeout(() => dialog.close(), 150);\n    return () => clearTimeout(timeout);\n  }, [open]);\n\n  // The native dialog does not stop the page behind it from scrolling.\n  useEffect(() => {\n    if (!open) return;\n    const root = document.documentElement;\n    const previous = root.style.overflow;\n    root.style.overflow = \"hidden\";\n    return () => {\n      root.style.overflow = previous;\n    };\n  }, [open]);\n\n  const select = useCallback(\n    (value: string) => {\n      items.find((item) => itemValue(item) === value)?.onSelect?.();\n      onOpenChange(false);\n    },\n    [items, onOpenChange],\n  );\n\n  const onKeyDown = (event: KeyboardEvent<HTMLDialogElement>) => {\n    if (event.key === \"Enter\") {\n      event.preventDefault();\n      if (activeValue !== undefined) select(activeValue);\n      return;\n    }\n    if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return;\n    event.preventDefault();\n    if (items.length === 0) return;\n    const index = items.findIndex((item) => itemValue(item) === activeValue);\n    const next = event.key === \"ArrowDown\" ? index + 1 : index - 1;\n    setActiveValue(itemValue(items[(next + items.length) % items.length]));\n  };\n\n  const ctx = useMemo<CommandContextValue>(\n    () => ({ query, setQuery, activeValue, setActiveValue, select, count: items.length, listboxId }),\n    [query, activeValue, items, listboxId, select],\n  );\n\n  return (\n    <dialog\n      ref={ref}\n      aria-label={label}\n      data-shown={shown}\n      onKeyDown={onKeyDown}\n      onCancel={(event) => {\n        event.preventDefault();\n        onOpenChange(false);\n      }}\n      onClose={() => {\n        if (open) onOpenChange(false);\n      }}\n      className=\"group fixed inset-0 m-0 h-dvh max-h-none w-screen max-w-none items-start justify-center overflow-hidden bg-transparent p-4 pt-[12vh] text-foreground backdrop:bg-transparent open:flex\"\n    >\n      <button\n        type=\"button\"\n        aria-label=\"Close\"\n        tabIndex={-1}\n        onClick={() => onOpenChange(false)}\n        className=\"absolute inset-0 cursor-default bg-black/40 opacity-0 transition-opacity duration-150 ease-out group-data-[shown=true]:opacity-100 group-data-[shown=true]:duration-200 motion-reduce:transition-none\"\n      />\n      <CommandContext.Provider value={ctx}>\n        <div\n          className={cn(\n            \"relative flex w-full max-w-lg flex-col overflow-hidden rounded-2xl bg-background\",\n            \"shadow-[0_0_0_1px_var(--border-strong),0_24px_60px_-20px_rgb(0_0_0/0.45)]\",\n            \"scale-[0.97] opacity-0 transition-[opacity,scale] duration-150 ease-out\",\n            \"group-data-[shown=true]:scale-100 group-data-[shown=true]:opacity-100 group-data-[shown=true]:duration-200\",\n            \"motion-reduce:transition-none\",\n            className,\n          )}\n        >\n          {children}\n        </div>\n      </CommandContext.Provider>\n    </dialog>\n  );\n}\n\nexport type CommandInputProps = Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"onChange\" | \"role\"\n>;\n\nexport function CommandInput({\n  className,\n  placeholder = \"Type a command or search...\",\n  ...props\n}: CommandInputProps) {\n  const { query, setQuery, listboxId, activeValue } = useCommand(\"CommandInput\");\n\n  return (\n    <div className=\"flex items-center gap-2.5 border-b border-border px-4\">\n      <Search aria-hidden=\"true\" className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n      <input\n        role=\"combobox\"\n        aria-expanded=\"true\"\n        aria-controls={listboxId}\n        aria-activedescendant={activeValue ? `${listboxId}-${activeValue}` : undefined}\n        autoComplete=\"off\"\n        spellCheck={false}\n        value={query}\n        onChange={(event) => setQuery(event.target.value)}\n        placeholder={placeholder}\n        className={cn(\n          \"h-12 w-full bg-transparent text-base text-foreground outline-none placeholder:text-muted-foreground sm:text-sm\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\nexport function CommandList({ className, children }: { className?: string; children: ReactNode }) {\n  const { listboxId } = useCommand(\"CommandList\");\n  return (\n    <div\n      id={listboxId}\n      role=\"listbox\"\n      className={cn(\"flex max-h-80 flex-col gap-1 overflow-y-auto p-2\", className)}\n    >\n      {children}\n    </div>\n  );\n}\n\n/** Shown in place of the list once nothing in it matches the query. */\nexport function CommandEmpty({ children }: { children: ReactNode }) {\n  const { count } = useCommand(\"CommandEmpty\");\n  if (count > 0) return null;\n  return <p className=\"px-4 py-8 text-center text-sm text-muted-foreground\">{children}</p>;\n}\n\nexport interface CommandGroupProps {\n  heading?: ReactNode;\n  className?: string;\n  children: ReactNode;\n}\n\n/** A labeled cluster of items. Collapses along with its heading once every item in it filters out. */\nexport function CommandGroup({ heading, className, children }: CommandGroupProps) {\n  const { query } = useCommand(\"CommandGroup\");\n  if (collectItems(children, query).length === 0) return null;\n  return (\n    <div className={cn(\"flex flex-col gap-1\", className)}>\n      {heading ? (\n        <div className=\"px-2.5 pt-2 pb-1 text-xs font-medium text-muted-foreground\">{heading}</div>\n      ) : null}\n      {children}\n    </div>\n  );\n}\n\nexport function CommandSeparator({ className }: { className?: string }) {\n  return <hr className={cn(\"mx-2 my-1 border-t border-border\", className)} />;\n}\n\n/** A selectable row. Hidden entirely once it stops matching the query. */\nexport function CommandItem({\n  value,\n  keywords,\n  disabled = false,\n  className,\n  children,\n}: CommandItemProps) {\n  const { query, activeValue, setActiveValue, select, listboxId } = useCommand(\"CommandItem\");\n  const ref = useRef<HTMLButtonElement>(null);\n  const thisValue = itemValue({ value, children });\n  const active = activeValue === thisValue;\n\n  useEffect(() => {\n    if (active) ref.current?.scrollIntoView({ block: \"nearest\" });\n  }, [active]);\n\n  if (!isMatch({ value, keywords, children }, query)) return null;\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      id={`${listboxId}-${thisValue}`}\n      role=\"option\"\n      tabIndex={-1}\n      disabled={disabled}\n      aria-selected={active}\n      data-active={active && !disabled}\n      onPointerMove={() => {\n        if (!disabled) setActiveValue(thisValue);\n      }}\n      onClick={() => select(thisValue)}\n      className={cn(\n        \"flex min-h-9 w-full touch-manipulation items-center gap-2 rounded-lg px-2.5 text-left text-sm text-foreground outline-none transition-colors duration-100\",\n        \"data-[active=true]:bg-muted\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        className,\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}