{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"morphing-search","type":"registry:block","title":"Morphing Search","description":"Search field or compact icon that morphs into a glass results surface, whether opened by click or keyboard shortcut.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/morphing-search.tsx","type":"registry:component","target":"@components/motion/morphing-search.tsx","content":"\"use client\";\n// beui.dev/components/blocks/morphing-search\n\nimport { type LucideIcon, Search } from \"lucide-react\";\nimport {\n\tAnimatePresence,\n\tLayoutGroup,\n\tmotion,\n\ttype Transition,\n\tuseReducedMotion,\n} from \"motion/react\";\nimport {\n\ttype KeyboardEvent as ReactKeyboardEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseId,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n// Keeps the Wallet Card feel with a little more time to read the morph.\nconst SEARCH_MORPH: Transition = {\n\ttype: \"spring\",\n\tduration: 0.58,\n\tbounce: 0.22,\n};\n\n// Keep the spring on the shell, but unfold complex clip-path values with the\n// same progressive tween as Morph Popover so the content never snaps ahead.\nconst SEARCH_CLIP_TRANSITION: Transition = {\n\tduration: 0.32,\n\tease: EASE_OUT,\n};\n\nexport type MorphingSearchItem = {\n\tid: string;\n\ttitle: string;\n\tdescription?: string;\n\tkeywords?: string[];\n\ticon?: LucideIcon;\n\tonSelect?: () => void;\n};\n\nexport interface MorphingSearchProps {\n\titems: MorphingSearchItem[];\n\tplaceholder?: string;\n\tshortcut?: string;\n\t/** Render the closed trigger as a compact search icon. */\n\ticonOnly?: boolean;\n\temptyMessage?: string;\n\topen?: boolean;\n\tdefaultOpen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tonQueryChange?: (query: string) => void;\n\tonSelect?: (item: MorphingSearchItem) => void;\n\tclassName?: string;\n}\n\ntype AnchorRect = {\n\ttop: number;\n\tleft: number;\n\twidth: number;\n};\n\nfunction isEditableTarget(target: EventTarget | null) {\n\tif (!(target instanceof HTMLElement)) return false;\n\treturn (\n\t\ttarget.isContentEditable ||\n\t\ttarget instanceof HTMLInputElement ||\n\t\ttarget instanceof HTMLTextAreaElement ||\n\t\ttarget instanceof HTMLSelectElement\n\t);\n}\n\nexport function MorphingSearch({\n\titems,\n\tplaceholder = \"Search\",\n\tshortcut = \"f\",\n\ticonOnly = false,\n\temptyMessage = \"No results found.\",\n\topen: controlledOpen,\n\tdefaultOpen = false,\n\tonOpenChange,\n\tonQueryChange,\n\tonSelect,\n\tclassName,\n}: MorphingSearchProps) {\n\tconst [internalOpen, setInternalOpen] = useState(defaultOpen);\n\tconst [query, setQuery] = useState(\"\");\n\tconst [activeIndex, setActiveIndex] = useState(0);\n\tconst [mounted, setMounted] = useState(false);\n\tconst [backgroundScrollLocked, setBackgroundScrollLocked] =\n\t\tuseState(defaultOpen);\n\tconst [anchorRect, setAnchorRect] = useState<AnchorRect>({\n\t\ttop: 16,\n\t\tleft: 16,\n\t\twidth: 288,\n\t});\n\tconst open = controlledOpen ?? internalOpen;\n\tconst controlled = controlledOpen !== undefined;\n\tconst reduce = useReducedMotion();\n\tconst uid = useId();\n\tconst anchorRef = useRef<HTMLDivElement>(null);\n\tconst triggerRef = useRef<HTMLButtonElement>(null);\n\tconst inputRef = useRef<HTMLInputElement>(null);\n\tconst dialogRef = useRef<HTMLDivElement>(null);\n\tconst listRef = useRef<HTMLDivElement>(null);\n\tconst previousFocusRef = useRef<HTMLElement | null>(null);\n\tconst wasOpenRef = useRef(open);\n\tconst transition: Transition = reduce ? { duration: 0 } : SPRING_LAYOUT;\n\tconst morphTransition: Transition = reduce ? { duration: 0 } : SEARCH_MORPH;\n\n\tconst setOpen = useCallback(\n\t\t(next: boolean) => {\n\t\t\tif (!controlled) setInternalOpen(next);\n\t\t\tonOpenChange?.(next);\n\t\t},\n\t\t[controlled, onOpenChange],\n\t);\n\n\tconst measureAnchor = useCallback(() => {\n\t\tconst rect = anchorRef.current?.getBoundingClientRect();\n\t\tif (!rect || rect.width === 0) return;\n\t\tsetAnchorRect({ top: rect.top, left: rect.left, width: rect.width });\n\t}, []);\n\n\tconst openSearch = useCallback(() => {\n\t\tmeasureAnchor();\n\t\tsetBackgroundScrollLocked(true);\n\t\tpreviousFocusRef.current =\n\t\t\tdocument.activeElement instanceof HTMLElement\n\t\t\t\t? document.activeElement\n\t\t\t\t: null;\n\t\tsetOpen(true);\n\t}, [measureAnchor, setOpen]);\n\n\tconst updateQuery = useCallback(\n\t\t(next: string) => {\n\t\t\tsetQuery(next);\n\t\t\tsetActiveIndex(0);\n\t\t\tonQueryChange?.(next);\n\t\t},\n\t\t[onQueryChange],\n\t);\n\n\tconst closeSearch = useCallback(() => {\n\t\tupdateQuery(\"\");\n\t\tsetOpen(false);\n\t}, [setOpen, updateQuery]);\n\n\tuseEffect(() => setMounted(true), []);\n\n\tuseEffect(() => {\n\t\tif (open) setBackgroundScrollLocked(true);\n\t}, [open]);\n\n\tuseEffect(() => {\n\t\tmeasureAnchor();\n\t\tconst anchor = anchorRef.current;\n\t\tconst observer =\n\t\t\tanchor && typeof ResizeObserver !== \"undefined\"\n\t\t\t\t? new ResizeObserver(measureAnchor)\n\t\t\t\t: null;\n\t\tif (anchor) observer?.observe(anchor);\n\t\twindow.addEventListener(\"resize\", measureAnchor);\n\t\tdocument.addEventListener(\"scroll\", measureAnchor, true);\n\t\twindow.visualViewport?.addEventListener(\"resize\", measureAnchor);\n\t\twindow.visualViewport?.addEventListener(\"scroll\", measureAnchor);\n\t\treturn () => {\n\t\t\tobserver?.disconnect();\n\t\t\twindow.removeEventListener(\"resize\", measureAnchor);\n\t\t\tdocument.removeEventListener(\"scroll\", measureAnchor, true);\n\t\t\twindow.visualViewport?.removeEventListener(\"resize\", measureAnchor);\n\t\t\twindow.visualViewport?.removeEventListener(\"scroll\", measureAnchor);\n\t\t};\n\t}, [measureAnchor]);\n\n\tuseEffect(() => {\n\t\tif (!backgroundScrollLocked) return;\n\n\t\tconst preventBackgroundWheel = (event: WheelEvent) => {\n\t\t\tconst target = event.target;\n\t\t\tif (target instanceof Node && listRef.current?.contains(target)) return;\n\t\t\tevent.preventDefault();\n\t\t};\n\t\tconst preventBackgroundTouch = (event: TouchEvent) => {\n\t\t\tconst target = event.target;\n\t\t\tif (target instanceof Node && listRef.current?.contains(target)) return;\n\t\t\tevent.preventDefault();\n\t\t};\n\n\t\tdocument.addEventListener(\"wheel\", preventBackgroundWheel, {\n\t\t\tpassive: false,\n\t\t});\n\t\tdocument.addEventListener(\"touchmove\", preventBackgroundTouch, {\n\t\t\tpassive: false,\n\t\t});\n\t\treturn () => {\n\t\t\tdocument.removeEventListener(\"wheel\", preventBackgroundWheel);\n\t\t\tdocument.removeEventListener(\"touchmove\", preventBackgroundTouch);\n\t\t};\n\t}, [backgroundScrollLocked]);\n\n\tuseEffect(() => {\n\t\tconst handleShortcut = (event: KeyboardEvent) => {\n\t\t\tif (event.key === \"Escape\" && open) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tcloseSearch();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!open &&\n\t\t\t\tshortcut &&\n\t\t\t\tevent.key.toLowerCase() === shortcut.toLowerCase() &&\n\t\t\t\t!event.repeat &&\n\t\t\t\t!event.metaKey &&\n\t\t\t\t!event.ctrlKey &&\n\t\t\t\t!event.altKey &&\n\t\t\t\t!event.shiftKey &&\n\t\t\t\t!isEditableTarget(event.target)\n\t\t\t) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\topenSearch();\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"keydown\", handleShortcut);\n\t\treturn () => window.removeEventListener(\"keydown\", handleShortcut);\n\t}, [closeSearch, open, openSearch, shortcut]);\n\n\tuseEffect(() => {\n\t\tif (open) {\n\t\t\tupdateQuery(\"\");\n\t\t\tconst frame = requestAnimationFrame(() => inputRef.current?.focus());\n\t\t\treturn () => cancelAnimationFrame(frame);\n\t\t}\n\n\t\tif (wasOpenRef.current) {\n\t\t\tconst frame = requestAnimationFrame(() => {\n\t\t\t\tconst previousFocus = previousFocusRef.current;\n\t\t\t\tconst focusTarget = previousFocus?.isConnected\n\t\t\t\t\t? previousFocus\n\t\t\t\t\t: triggerRef.current;\n\t\t\t\tfocusTarget?.focus();\n\t\t\t});\n\t\t\treturn () => cancelAnimationFrame(frame);\n\t\t}\n\t}, [open, updateQuery]);\n\n\tuseEffect(() => {\n\t\twasOpenRef.current = open;\n\t}, [open]);\n\n\tconst filteredItems = useMemo(() => {\n\t\tconst needle = query.trim().toLowerCase();\n\t\tif (!needle) return items;\n\n\t\treturn items.filter((item) =>\n\t\t\t[item.title, item.description ?? \"\", ...(item.keywords ?? [])]\n\t\t\t\t.join(\" \")\n\t\t\t\t.toLowerCase()\n\t\t\t\t.includes(needle),\n\t\t);\n\t}, [items, query]);\n\n\tuseEffect(() => {\n\t\tif (activeIndex < filteredItems.length) return;\n\t\tsetActiveIndex(Math.max(0, filteredItems.length - 1));\n\t}, [activeIndex, filteredItems.length]);\n\n\tuseEffect(() => {\n\t\tif (!open) return;\n\t\tlistRef.current\n\t\t\t?.querySelector<HTMLElement>(`[data-index=\"${activeIndex}\"]`)\n\t\t\t?.scrollIntoView({ block: \"nearest\" });\n\t}, [activeIndex, open]);\n\n\tconst selectItem = useCallback(\n\t\t(item: MorphingSearchItem) => {\n\t\t\titem.onSelect?.();\n\t\t\tonSelect?.(item);\n\t\t\tcloseSearch();\n\t\t},\n\t\t[closeSearch, onSelect],\n\t);\n\n\tconst handleDialogKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n\t\tif (event.key === \"ArrowDown\") {\n\t\t\tevent.preventDefault();\n\t\t\tif (filteredItems.length === 0) return;\n\t\t\tsetActiveIndex((current) =>\n\t\t\t\tMath.min(current + 1, filteredItems.length - 1),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"ArrowUp\") {\n\t\t\tevent.preventDefault();\n\t\t\tsetActiveIndex((current) => Math.max(current - 1, 0));\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === \"Enter\") {\n\t\t\tevent.preventDefault();\n\t\t\tconst item = filteredItems[activeIndex];\n\t\t\tif (item) selectItem(item);\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key !== \"Tab\" || !dialogRef.current) return;\n\t\tconst focusable = Array.from(\n\t\t\tdialogRef.current.querySelectorAll<HTMLElement>(\n\t\t\t\t'input, button:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n\t\t\t),\n\t\t);\n\t\tconst first = focusable[0];\n\t\tconst last = focusable.at(-1);\n\t\tif (!first || !last) return;\n\n\t\tif (event.shiftKey && document.activeElement === first) {\n\t\t\tevent.preventDefault();\n\t\t\tlast.focus();\n\t\t} else if (!event.shiftKey && document.activeElement === last) {\n\t\t\tevent.preventDefault();\n\t\t\tfirst.focus();\n\t\t}\n\t};\n\n\tconst shellLayoutId = `${uid}-shell`;\n\tconst listboxId = `${uid}-results`;\n\tconst panelWidth = mounted\n\t\t? Math.max(\n\t\t\t\tanchorRect.width,\n\t\t\t\tMath.min(448, window.innerWidth - anchorRect.left - 16),\n\t\t\t)\n\t\t: anchorRect.width;\n\tconst resultsHeight = mounted\n\t\t? Math.max(96, Math.min(288, window.innerHeight - anchorRect.top - 80))\n\t\t: 288;\n\tconst collapsedContentClip = `inset(0px ${Math.max(\n\t\t0,\n\t\tpanelWidth - anchorRect.width,\n\t)}px ${resultsHeight}px 0px round 12px)`;\n\tconst expandedContentClip = \"inset(0px 0px 0px 0px round 12px)\";\n\n\tconst overlay = mounted\n\t\t? createPortal(\n\t\t\t\t<div\n\t\t\t\t\taria-hidden={!open}\n\t\t\t\t\tinert={!open}\n\t\t\t\t\tclassName=\"pointer-events-none fixed inset-0 z-50\"\n\t\t\t\t>\n\t\t\t\t\t<AnimatePresence\n\t\t\t\t\t\tinitial={false}\n\t\t\t\t\t\tmode=\"popLayout\"\n\t\t\t\t\t\tonExitComplete={() => setBackgroundScrollLocked(false)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{open ? (\n\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\tkey=\"morphing-search-overlay\"\n\t\t\t\t\t\t\t\tclassName=\"fixed inset-0\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\taria-label=\"Close search\"\n\t\t\t\t\t\t\t\t\tclassName=\"pointer-events-auto absolute inset-0 cursor-default bg-transparent\"\n\t\t\t\t\t\t\t\t\tonClick={closeSearch}\n\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\t\tlayoutId={shellLayoutId}\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t\tclassName=\"fixed z-10 rounded-xl bg-background/90 backdrop-blur-xl\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\ttop: anchorRect.top,\n\t\t\t\t\t\t\t\t\t\tleft: anchorRect.left,\n\t\t\t\t\t\t\t\t\t\twidth: panelWidth,\n\t\t\t\t\t\t\t\t\t\theight: 48 + resultsHeight,\n\t\t\t\t\t\t\t\t\t\tboxShadow: \"inset 0 0 0 1px var(--color-border)\",\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\ttransition={morphTransition}\n\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\t\tref={dialogRef}\n\t\t\t\t\t\t\t\t\trole=\"dialog\"\n\t\t\t\t\t\t\t\t\taria-modal=\"true\"\n\t\t\t\t\t\t\t\t\taria-label=\"Search\"\n\t\t\t\t\t\t\t\t\tonKeyDown={handleDialogKeyDown}\n\t\t\t\t\t\t\t\t\tinitial={\n\t\t\t\t\t\t\t\t\t\treduce\n\t\t\t\t\t\t\t\t\t\t\t? false\n\t\t\t\t\t\t\t\t\t\t\t: { opacity: 0, clipPath: collapsedContentClip }\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tanimate={{ opacity: 1, clipPath: expandedContentClip }}\n\t\t\t\t\t\t\t\t\texit={{\n\t\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\t\tclipPath: collapsedContentClip,\n\t\t\t\t\t\t\t\t\t\ttransition: reduce\n\t\t\t\t\t\t\t\t\t\t\t? { duration: 0 }\n\t\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tclipPath: SEARCH_CLIP_TRANSITION,\n\t\t\t\t\t\t\t\t\t\t\t\t\topacity: SEARCH_MORPH,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\ttransition={\n\t\t\t\t\t\t\t\t\t\treduce\n\t\t\t\t\t\t\t\t\t\t\t? { duration: 0 }\n\t\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tclipPath: SEARCH_CLIP_TRANSITION,\n\t\t\t\t\t\t\t\t\t\t\t\t\topacity: SEARCH_MORPH,\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tclassName=\"pointer-events-auto fixed z-20 overflow-hidden rounded-xl\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\ttop: anchorRect.top,\n\t\t\t\t\t\t\t\t\t\tleft: anchorRect.left,\n\t\t\t\t\t\t\t\t\t\twidth: panelWidth,\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\"flex h-12 items-center gap-2.5 border-b border-border\",\n\t\t\t\t\t\t\t\t\t\t\ticonOnly ? \"px-4\" : \"px-3.5\",\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span className=\"shrink-0\">\n\t\t\t\t\t\t\t\t\t\t\t<Search className=\"size-4 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<div className=\"flex h-10 min-w-0 flex-1 items-center\">\n\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\tref={inputRef}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue={query}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={(event) => updateQuery(event.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\t\t\t\t\t\t\t\taria-label={placeholder}\n\t\t\t\t\t\t\t\t\t\t\t\taria-expanded=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\taria-controls={listboxId}\n\t\t\t\t\t\t\t\t\t\t\t\taria-autocomplete=\"list\"\n\t\t\t\t\t\t\t\t\t\t\t\taria-activedescendant={\n\t\t\t\t\t\t\t\t\t\t\t\t\tfilteredItems.length > 0\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `${uid}-option-${activeIndex}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={placeholder}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"size-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<kbd className=\"flex h-7 shrink-0 items-center rounded-md border border-border px-2 text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\tEsc\n\t\t\t\t\t\t\t\t\t\t</kbd>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\t\t\tref={listRef}\n\t\t\t\t\t\t\t\t\t\tid={listboxId}\n\t\t\t\t\t\t\t\t\t\trole=\"listbox\"\n\t\t\t\t\t\t\t\t\t\taria-label=\"Search results\"\n\t\t\t\t\t\t\t\t\t\ttransition={reduce ? { duration: 0 } : undefined}\n\t\t\t\t\t\t\t\t\t\tvariants={\n\t\t\t\t\t\t\t\t\t\t\treduce\n\t\t\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclosed: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttransform: \"translateY(6px)\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tduration: 0.16,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdelay: 0.18,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tease: EASE_OUT,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topen: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttransform: \"translateY(0px)\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tduration: 0.16,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tease: EASE_OUT,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tinitial={\n\t\t\t\t\t\t\t\t\t\t\treduce\n\t\t\t\t\t\t\t\t\t\t\t\t? { opacity: 1, transform: \"translateY(0px)\" }\n\t\t\t\t\t\t\t\t\t\t\t\t: \"closed\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tanimate={\n\t\t\t\t\t\t\t\t\t\t\treduce\n\t\t\t\t\t\t\t\t\t\t\t\t? { opacity: 1, transform: \"translateY(0px)\" }\n\t\t\t\t\t\t\t\t\t\t\t\t: \"open\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\texit={reduce ? undefined : \"closed\"}\n\t\t\t\t\t\t\t\t\t\tclassName=\"overscroll-contain overflow-y-auto p-2\"\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tmaxHeight: resultsHeight,\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{filteredItems.length > 0 ? (\n\t\t\t\t\t\t\t\t\t\t\tfilteredItems.map((item, index) => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst Icon = item.icon;\n\t\t\t\t\t\t\t\t\t\t\t\tconst active = index === activeIndex;\n\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid={`${uid}-option-${index}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected={active}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata-index={index}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonMouseMove={() => setActiveIndex(index)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonFocus={() => setActiveIndex(index)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => selectItem(item)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"relative flex w-full items-center gap-2.5 rounded-lg px-3 py-2.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{active ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<motion.span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlayoutId={`${uid}-active-result`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute inset-0 rounded-lg bg-foreground/5\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttransition={transition}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{Icon ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Icon className=\"relative size-4 shrink-0 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"relative min-w-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"block truncate text-sm font-medium text-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.title}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.description ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"block truncate text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.description}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"px-3 py-8 text-center text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t{emptyMessage}\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</AnimatePresence>\n\t\t\t\t</div>,\n\t\t\t\tdocument.body,\n\t\t\t)\n\t\t: null;\n\n\treturn (\n\t\t<LayoutGroup id={uid}>\n\t\t\t<div\n\t\t\t\tref={anchorRef}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"relative\",\n\t\t\t\t\ticonOnly ? \"size-12\" : \"h-12 w-72 max-w-full\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{!open ? (\n\t\t\t\t\t<motion.button\n\t\t\t\t\t\tref={triggerRef}\n\t\t\t\t\t\tkey=\"morphing-search-trigger\"\n\t\t\t\t\t\tlayoutId={shellLayoutId}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\taria-haspopup=\"dialog\"\n\t\t\t\t\t\taria-expanded=\"false\"\n\t\t\t\t\t\taria-label={placeholder}\n\t\t\t\t\t\tonClick={openSearch}\n\t\t\t\t\t\ttransition={morphTransition}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tboxShadow: \"inset 0 0 0 1px var(--search-trigger-stroke)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"flex size-full items-center rounded-xl bg-background/60 text-left backdrop-blur-md outline-none [--search-trigger-stroke:var(--color-border)] hover:[--search-trigger-stroke:var(--color-border-strong)] focus-visible:ring-2 focus-visible:ring-ring\",\n\t\t\t\t\t\t\ticonOnly ? \"cursor-pointer justify-center\" : \"cursor-text px-3.5\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t></motion.button>\n\t\t\t\t) : null}\n\t\t\t\t<motion.div\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\tinitial={false}\n\t\t\t\t\tanimate={{ opacity: open ? 0 : 1 }}\n\t\t\t\t\ttransition={\n\t\t\t\t\t\treduce\n\t\t\t\t\t\t\t? { duration: 0 }\n\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\tduration: 0.1,\n\t\t\t\t\t\t\t\t\tdelay: open ? 0.1 : 0.12,\n\t\t\t\t\t\t\t\t\tease: EASE_OUT,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"pointer-events-none absolute inset-0 flex items-center\",\n\t\t\t\t\t\tbackgroundScrollLocked && \"z-[60]\",\n\t\t\t\t\t\ticonOnly ? \"justify-center\" : \"gap-2.5 px-3.5\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<Search className=\"size-4 shrink-0 text-muted-foreground\" />\n\t\t\t\t\t{iconOnly ? null : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<span className=\"min-w-0 flex-1 truncate text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t{placeholder}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{shortcut ? (\n\t\t\t\t\t\t\t\t<kbd className=\"flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md border border-border px-2 text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{shortcut.toUpperCase()}\n\t\t\t\t\t\t\t\t</kbd>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</motion.div>\n\t\t\t</div>\n\t\t\t{overlay}\n\t\t</LayoutGroup>\n\t);\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\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"}]}