"use client" import * as React from "react" import { usePathname, useRouter } from "next/navigation" import { IconArrowRight } from "@tabler/icons-react" import { useDocsSearch } from "fumadocs-core/search/client" import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react" import { Dialog as DialogPrimitive } from "radix-ui" import { type Color, type ColorPalette } from "@/lib/colors" import { trackEvent } from "@/lib/events" import { showMcpDocs } from "@/lib/flags" import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree" import { type source } from "@/lib/source" import { cn } from "@/lib/utils" import { useConfig } from "@/hooks/use-config" import { useMutationObserver } from "@/hooks/use-mutation-observer" import { copyToClipboardWithMeta } from "@/components/copy-button" import { Button } from "@/registry/new-york-v4/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/registry/new-york-v4/ui/command" import { Dialog, DialogDescription, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, } from "@/registry/new-york-v4/ui/dialog" import { Separator } from "@/registry/new-york-v4/ui/separator" import { Spinner } from "@/registry/new-york-v4/ui/spinner" export function CommandMenu({ tree, colors, blocks, navItems, ...props }: React.ComponentProps & { tree: typeof source.pageTree colors: ColorPalette[] blocks?: { name: string; description: string; categories: string[] }[] navItems?: { href: string; label: string }[] }) { const router = useRouter() const pathname = usePathname() const [config] = useConfig() const currentBase = getCurrentBase(pathname) const [open, setOpen] = React.useState(false) const [renderDelayedGroups, setRenderDelayedGroups] = React.useState(false) const [selectedType, setSelectedType] = React.useState< "color" | "page" | "component" | "block" | null >(null) const [copyPayload, setCopyPayload] = React.useState("") const { search, setSearch, query } = useDocsSearch({ type: "fetch", }) const packageManager = config.packageManager || "pnpm" // Track search queries with debouncing to avoid excessive tracking. const searchTimeoutRef = React.useRef(undefined) const lastTrackedQueryRef = React.useRef("") const trackSearchQuery = React.useCallback((query: string) => { const trimmedQuery = query.trim() // Only track if the query is different from the last tracked query and has content. if (trimmedQuery && trimmedQuery !== lastTrackedQueryRef.current) { lastTrackedQueryRef.current = trimmedQuery trackEvent({ name: "search_query", properties: { query: trimmedQuery, query_length: trimmedQuery.length, }, }) } }, []) const handleSearchChange = React.useCallback( (value: string) => { // Clear existing timeout. if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current) } // Set new timeout to debounce both search and tracking. searchTimeoutRef.current = setTimeout(() => { React.startTransition(() => { setSearch(value) trackSearchQuery(value) }) }, 500) }, [setSearch, trackSearchQuery] ) // Cleanup timeout on unmount. React.useEffect(() => { if (open) { const frame = requestAnimationFrame(() => { setRenderDelayedGroups(true) }) return () => { cancelAnimationFrame(frame) } } setRenderDelayedGroups(false) }, [open]) React.useEffect(() => { return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current) } } }, []) const commandFilter = React.useCallback( (value: string, searchValue: string, keywords?: string[]) => { const extendValue = value + " " + (keywords?.join(" ") || "") if (extendValue.toLowerCase().includes(searchValue.toLowerCase())) { return 1 } return 0 }, [] ) const handlePageHighlight = React.useCallback( (isComponent: boolean, item: { url: string; name?: React.ReactNode }) => { if (isComponent) { const componentName = item.url.split("/").pop() setSelectedType("component") setCopyPayload( `${packageManager} dlx shadcn@latest add ${componentName}` ) } else { setSelectedType("page") setCopyPayload("") } }, [packageManager, setSelectedType, setCopyPayload] ) const handleColorHighlight = React.useCallback( (color: Color) => { setSelectedType("color") setCopyPayload(color.className) }, [setSelectedType, setCopyPayload] ) const handleBlockHighlight = React.useCallback( (block: { name: string; description: string; categories: string[] }) => { setSelectedType("block") setCopyPayload(`${packageManager} dlx shadcn@latest add ${block.name}`) }, [setSelectedType, setCopyPayload, packageManager] ) const runCommand = React.useCallback( (command: () => unknown) => { setOpen(false) command() }, [setOpen] ) const navItemsSection = React.useMemo(() => { if (!navItems || navItems.length === 0) { return null } return ( {navItems.map((item) => ( { setSelectedType("page") setCopyPayload("") }} onSelect={() => { runCommand(() => router.push(item.href)) }} > {item.label} ))} ) }, [navItems, runCommand, router]) const pageGroupsSection = React.useMemo(() => { return tree.children.map((group) => { if (group.type !== "folder") { return null } const pages = getPagesFromFolder(group, currentBase).filter((item) => { if (!showMcpDocs && item.url.includes("/mcp")) { return false } return true }) if (pages.length === 0) { return null } return ( {pages.map((item) => { const isComponent = item.url.includes("/components/") return ( handlePageHighlight(isComponent, item)} onSelect={() => { runCommand(() => router.push(item.url)) }} > {isComponent ? (
) : ( )} {item.name} ) })} ) }) }, [tree.children, currentBase, handlePageHighlight, runCommand, router]) const colorGroupsSection = React.useMemo(() => { return colors.map((colorPalette) => ( {colorPalette.colors.map((color) => ( handleColorHighlight(color)} onSelect={() => { runCommand(() => copyToClipboardWithMeta(color.oklch, { name: "copy_color", properties: { color: color.oklch }, }) ) }} >
{color.className} {color.oklch} ))} )) }, [colors, handleColorHighlight, runCommand]) const blocksSection = React.useMemo(() => { if (!blocks || blocks.length === 0) { return null } return ( {blocks.map((block) => ( { handleBlockHighlight(block) }} keywords={[ "block", block.name, block.description, ...block.categories, ]} onSelect={() => { runCommand(() => router.push(`/blocks/${block.categories[0]}#${block.name}`) ) }} > {block.description} {block.name} ))} ) }, [blocks, handleBlockHighlight, runCommand, router]) React.useEffect(() => { const down = (e: KeyboardEvent) => { if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") { if ( (e.target instanceof HTMLElement && e.target.isContentEditable) || e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement ) { return } e.preventDefault() setOpen((open) => !open) } if (e.key === "c" && (e.metaKey || e.ctrlKey)) { runCommand(() => { if (selectedType === "color") { copyToClipboardWithMeta(copyPayload, { name: "copy_color", properties: { color: copyPayload }, }) } if (selectedType === "block") { copyToClipboardWithMeta(copyPayload, { name: "copy_npm_command", properties: { command: copyPayload, pm: packageManager }, }) } if (selectedType === "page" || selectedType === "component") { copyToClipboardWithMeta(copyPayload, { name: "copy_npm_command", properties: { command: copyPayload, pm: packageManager }, }) } }) } } document.addEventListener("keydown", down) return () => document.removeEventListener("keydown", down) }, [copyPayload, runCommand, selectedType, packageManager]) return ( Search documentation... Search for a command to run...
{query.isLoading && (
)}
{query.isLoading ? "Searching..." : "No results found."} {navItemsSection} {renderDelayedGroups ? ( <> {pageGroupsSection} {colorGroupsSection} {blocksSection} ) : null}
{" "} {selectedType === "page" || selectedType === "component" ? "Go to Page" : null} {selectedType === "color" ? "Copy OKLCH" : null}
{copyPayload && ( <>
C {copyPayload}
)}
) } function CommandMenuItem({ children, className, onHighlight, ...props }: React.ComponentProps & { onHighlight?: () => void "data-selected"?: string "aria-selected"?: string }) { const ref = React.useRef(null) useMutationObserver(ref, (mutations) => { mutations.forEach((mutation) => { if ( mutation.type === "attributes" && mutation.attributeName === "aria-selected" && ref.current?.getAttribute("aria-selected") === "true" ) { onHighlight?.() } }) }) return ( {children} ) } function CommandMenuKbd({ className, ...props }: React.ComponentProps<"kbd">) { return ( ) } type Query = Awaited>["query"] function SearchResults({ setOpen, query, search, }: { setOpen: (open: boolean) => void query: Query search: string }) { const router = useRouter() const uniqueResults = React.useMemo(() => { if (!query.data || !Array.isArray(query.data)) { return [] } return query.data.filter( (item, index, self) => !( item.type === "text" && item.content.trim().split(/\s+/).length <= 1 ) && index === self.findIndex((t) => t.content === item.content) ) }, [query.data]) if (!search.trim()) { return null } if (!query.data || query.data === "empty") { return null } if (query.data && uniqueResults.length === 0) { return null } return ( {uniqueResults.map((item) => { return ( { router.push(item.url) setOpen(false) }} className="h-9 rounded-md border border-transparent px-3! font-normal data-[selected=true]:border-input data-[selected=true]:bg-input/50" keywords={[item.content]} value={`${item.content} ${item.type}`} >
{item.content}
) })}
) } function DialogContent({ className, children, ...props }: React.ComponentProps & { showCloseButton?: boolean }) { return ( {/* */} {children} ) }