"use client" import * as React from "react" import { SearchIcon } from "lucide-react" import { cn } from "@/lib/utils" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" interface CommandContextValue { search: string setSearch: (value: string) => void selectedIndex: number setSelectedIndex: (index: number) => void items: React.RefObject registerItem: (element: HTMLDivElement | null, index: number) => void filter: (value: string, search: string) => boolean } const CommandContext = React.createContext(null) function useCommand() { const context = React.useContext(CommandContext) if (!context) { throw new Error("useCommand must be used within a Command") } return context } interface CommandProps extends React.HTMLAttributes { filter?: (value: string, search: string) => boolean shouldFilter?: boolean } function Command({ className, children, filter, shouldFilter = true, ...props }: CommandProps) { const [search, setSearch] = React.useState("") const [selectedIndex, setSelectedIndex] = React.useState(0) const items = React.useRef([]) const defaultFilter = React.useCallback((value: string, search: string) => { if (!shouldFilter) return true if (search.length === 0) return true return value.toLowerCase().includes(search.toLowerCase()) }, [shouldFilter]) const registerItem = React.useCallback((element: HTMLDivElement | null, index: number) => { if (element) { items.current[index] = element } }, []) const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => { const visibleItems = items.current.filter((item) => item && !item.hidden) switch (e.key) { case "ArrowDown": e.preventDefault() setSelectedIndex((prev) => { const next = prev + 1 return next >= visibleItems.length ? 0 : next }) break case "ArrowUp": e.preventDefault() setSelectedIndex((prev) => { const next = prev - 1 return next < 0 ? visibleItems.length - 1 : next }) break case "Enter": e.preventDefault() const selectedItem = visibleItems[selectedIndex] if (selectedItem) { selectedItem.click() } break case "Home": e.preventDefault() setSelectedIndex(0) break case "End": e.preventDefault() setSelectedIndex(visibleItems.length - 1) break } }, [selectedIndex]) React.useEffect(() => { setSelectedIndex(0) }, [search]) return (
{children}
) } function CommandDialog({ title = "Command Palette", description = "Search for a command to run...", children, className, showCloseButton = false, ...props }: React.ComponentProps & { title?: string description?: string className?: string showCloseButton?: boolean }) { return ( {title} {description} {children} ) } function CommandInput({ className, ...props }: React.InputHTMLAttributes) { const { search, setSearch } = useCommand() return (
setSearch(e.target.value)} className={cn( "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50", className )} {...props} />
) } function CommandList({ className, ...props }: React.HTMLAttributes) { return (
) } function CommandEmpty({ className, children, ...props }: React.HTMLAttributes) { const { search, items } = useCommand() const [isEmpty, setIsEmpty] = React.useState(false) React.useEffect(() => { const checkEmpty = () => { const visibleItems = search.length === 0 ? items.current.filter((item) => item) : items.current.filter((item) => item && !item.hidden) setIsEmpty(search.length > 0 && visibleItems.length === 0) } // Use a small delay to let items update their visibility const timeout = setTimeout(checkEmpty, 10) return () => clearTimeout(timeout) }, [search, items]) if (!isEmpty) return null return (
{children || "No results found."}
) } interface CommandGroupProps extends React.HTMLAttributes { heading?: React.ReactNode } function CommandGroup({ className, heading, children, ...props }: CommandGroupProps) { const groupRef = React.useRef(null) const { search } = useCommand() const [hasVisibleItems, setHasVisibleItems] = React.useState(true) React.useEffect(() => { if (search.length === 0) { setHasVisibleItems(true) return } const checkVisibility = () => { if (groupRef.current) { const items = groupRef.current.querySelectorAll('[data-slot="command-item"]') const visibleItems = Array.from(items).filter( (item) => !(item as HTMLElement).hidden ) setHasVisibleItems(visibleItems.length > 0) } } const timeout = setTimeout(checkVisibility, 10) return () => clearTimeout(timeout) }, [search]) return ( ) } function CommandSeparator({ className, ...props }: React.HTMLAttributes) { return (
) } interface CommandItemProps extends React.HTMLAttributes { disabled?: boolean value?: string onSelect?: (value: string) => void keywords?: string[] } const itemIndexCounter = { current: 0 } function getTextFromChildren(children: React.ReactNode): string { if (typeof children === "string") return children if (typeof children === "number") return String(children) if (Array.isArray(children)) return children.map(getTextFromChildren).join(" ") if (React.isValidElement(children) && children.props.children) { return getTextFromChildren(children.props.children) } return "" } function CommandItem({ className, disabled, value, onSelect, keywords = [], children, ...props }: CommandItemProps) { const { search, selectedIndex, setSelectedIndex, registerItem, filter } = useCommand() const ref = React.useRef(null) const indexRef = React.useRef(-1) React.useLayoutEffect(() => { indexRef.current = itemIndexCounter.current++ return () => { itemIndexCounter.current = Math.max(0, itemIndexCounter.current - 1) } }, []) React.useEffect(() => { registerItem(ref.current, indexRef.current) }, [registerItem]) const searchableText = value || getTextFromChildren(children) const allSearchableText = [searchableText, ...keywords].join(" ") const isVisible = search.length === 0 ? true : filter(allSearchableText, search) const isSelected = selectedIndex === indexRef.current && isVisible const handleSelect = () => { if (disabled) return onSelect?.(searchableText) } const handleMouseEnter = () => { if (!disabled) { setSelectedIndex(indexRef.current) } } return ( ) } function CommandShortcut({ className, ...props }: React.HTMLAttributes) { return ( ) } export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, }