{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"combobox","type":"registry:ui","title":"Combobox","description":"A single-choice text field over a closed set of options — typing filters a portalled listbox, and a value outside the list can never be selected.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["forms"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/combobox/Combobox.tsx","content":"\"use client\";\n\nimport {\n\tforwardRef,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n\tuseSyncExternalStore,\n} from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, FocusEvent, KeyboardEvent } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useConstant } from \"../../internals/dom/ssr\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { useField } from \"../../internals/field\";\nimport { createListbox } from \"../../internals/listbox\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { ComboboxPanel } from \"./ComboboxPanel\";\nimport { defaultFilter } from \"./match\";\nimport { COMBOBOX_KEY } from \"./types\";\nimport type { ComboboxContext, ComboboxOption } from \"./types\";\nimport \"./combobox.css\";\n\nexport type { ComboboxOption };\n\n/**\n * Every other native `<input>` attribute and listener (`aria-*`, `data-*`,\n * `autoComplete`, `inputMode`, `form`, `onFocus`, `onBlur`, `onKeyDown`,\n * `onChange`, ...) is forwarded to the text input. The managed ARIA (`role`,\n * `aria-expanded`, `aria-controls`, `aria-activedescendant`, `aria-haspopup`,\n * `aria-autocomplete`, `aria-invalid`) stays the component's own.\n */\nexport interface ComboboxProps extends Omit<\n\tComponentPropsWithoutRef<\"input\">,\n\t| \"className\"\n\t| \"id\"\n\t| \"name\"\n\t| \"value\"\n\t| \"disabled\"\n\t| \"required\"\n\t| \"placeholder\"\n\t| \"type\"\n\t| \"role\"\n\t| \"children\"\n> {\n\t/** The closed set of selectable options. A value outside this list is never valid. */\n\toptions: ComboboxOption[];\n\t/**\n\t * The selected option's value, or \"\" when nothing is selected.\n\t *\n\t * Controlled when passed: the caller owns it and writes the value back\n\t * from `onValueChange`. Left out entirely, the component keeps its own\n\t * copy and still reports every change — the React spelling of the\n\t * source's `$bindable(\"\")`.\n\t */\n\tvalue?: string;\n\t/** Called with the new value whenever the selection commits — a row click, Enter on the active row, or a caller writing `value` never fires this itself. */\n\tonValueChange?: (value: string) => void;\n\t/** Shown while the field is empty and nothing is selected. */\n\tplaceholder?: string;\n\t/** Blocks focus and typing; excluded from form submission. Overridden by a surrounding FormField. */\n\tdisabled?: boolean;\n\t/** Native `required`. Overridden by a surrounding FormField. */\n\trequired?: boolean;\n\t/** Drives the error border and `aria-invalid`. Overridden by a surrounding FormField. */\n\tinvalid?: boolean;\n\t/** Element id. Overridden by a surrounding FormField's own `controlId`. */\n\tid?: string;\n\t/** Native `name` — submitted via a hidden input carrying `value`, since the visible field displays the option's label, not its value. */\n\tname?: string;\n\t/** Accessible name — for a control with no visible Label next to it. */\n\tlabel?: string;\n\t/** Matches an option against the current query. Default: case-insensitive substring match on `label`. */\n\tfilter?: (option: ComboboxOption, query: string) => boolean;\n\t/** Shown in the panel when no option matches the current query. */\n\temptyMessage?: string;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\nfunction findOption(options: ComboboxOption[], val: string): ComboboxOption | undefined {\n\treturn options.find((o) => o.value === val);\n}\n\n/**\n * The filtered list, as a pure module-level function rather than only as the\n * memo below, so it can also be called directly from handlers that just wrote\n * `query` in the same pass (see `handleInput`) — reading the memoized\n * `filteredOptions` there instead would mean reading a value computed from\n * the pre-write render.\n *\n * While the visible text still equals the selected option's own label — true\n * both before the user has touched the field at all and right after a fresh\n * selection — nothing is filtered out, so opening the panel browses the whole\n * list rather than the one row that happens to still be typed in. The moment\n * the visible text diverges from that label, real filtering kicks in.\n */\nfunction computeFilteredOptions(\n\toptions: ComboboxOption[],\n\tvalue: string,\n\tfilter: ((option: ComboboxOption, query: string) => boolean) | undefined,\n\tq: string\n): ComboboxOption[] {\n\tconst selectedLabel = findOption(options, value)?.label ?? \"\";\n\tif (q === selectedLabel) return options;\n\treturn options.filter((o) => (filter ? filter(o, q) : defaultFilter(o.label, q)));\n}\n\n// -1 is the value the listbox starts at on both sides, so the server render\n// and the hydration render agree that nothing is active.\nconst activeIndexServerSnapshot = () => -1;\n\n/**\n * A text input over a closed set of options, with a portalled listbox panel.\n *\n * The input element arrives through the ref channel rather than a `ref` prop,\n * per PORTING.md — the Svelte source declares `ref = $bindable(null)`.\n *\n * Rest props are spread onto the `<input>` BEFORE the invariants, as the source\n * spreads `...restProps` there.\n */\nexport const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(function Combobox(\n\t{\n\t\toptions,\n\t\tvalue: valueProp,\n\t\tonValueChange,\n\t\tplaceholder,\n\t\tdisabled = false,\n\t\trequired = false,\n\t\tinvalid = false,\n\t\tid,\n\t\tname,\n\t\tlabel,\n\t\tfilter,\n\t\temptyMessage = \"No results\",\n\t\tclassName,\n\t\tsound = false,\n\t\tonChange,\n\t\tonFocus,\n\t\tonBlur,\n\t\tonKeyDown,\n\t\t...restProps\n\t},\n\tforwardedRef\n) {\n\t// Undefined outside a FormField — every `effective*` below then falls back\n\t// to this component's own props instead of the context, so the control\n\t// works standalone exactly as it does wrapped. Same convention as Input.\n\tconst field = useField();\n\n\tconst uid = useFancyId();\n\tconst effectiveId = field?.controlId ?? id ?? `${uid}-input`;\n\tconst effectiveDisabled = field?.disabled ?? disabled;\n\tconst effectiveRequired = field?.required ?? required;\n\tconst effectiveInvalid = field?.invalid ?? invalid;\n\tconst panelId = `${uid}-listbox`;\n\n\tconst playCue = useSoundCue(sound);\n\n\t// The source's `value = $bindable(\"\")`: a consumer can bind it, or hand it\n\t// a plain value and let the component keep writing its own copy. React has\n\t// no such channel, so the prop SEEDS this copy and re-seeds it whenever the\n\t// consumer actually changes it — which is exactly what a non-bound\n\t// `$bindable` prop does, and is what keeps \"a plain `value` plus\n\t// `onValueChange`\" a working combination rather than a field that reverts\n\t// to the seed on the next blur. A consumer that does drive `value` from\n\t// `onValueChange` is fully controlled.\n\tconst [value, setValueState] = useState(valueProp ?? \"\");\n\tconst [lastValueProp, setLastValueProp] = useState(valueProp);\n\tif (lastValueProp !== valueProp) {\n\t\tsetLastValueProp(valueProp);\n\t\tsetValueState(valueProp ?? \"\");\n\t}\n\n\t// The label of the currently selected option is also the field's resting\n\t// text — computed here as the initial state, not through an effect, so it\n\t// is correct on the very first server-rendered paint too, not just after\n\t// hydration.\n\tconst [query, setQueryState] = useState(() => findOption(options, value)?.label ?? \"\");\n\n\t// The same string, written SYNCHRONOUSLY. `handleInput` writes the query\n\t// and then calls `listbox.moveToEdge` in the same turn, and the listbox's\n\t// `count`/`enabled` callbacks must see that keystroke's result — a live\n\t// ref (an insertion effect, i.e. the next commit) would still be holding\n\t// the pre-keystroke text. This is the React shape of the source's own note\n\t// about not reading a derived in the pass that wrote its dependency.\n\tconst queryRef = useRef(query);\n\tconst setQuery = useCallback((next: string) => {\n\t\tqueryRef.current = next;\n\t\tsetQueryState(next);\n\t}, []);\n\n\tconst [open, setOpen] = useState(false);\n\n\tconst filteredOptions = useMemo(\n\t\t() => computeFilteredOptions(options, value, filter, query),\n\t\t[options, value, filter, query]\n\t);\n\n\t// Read by the listbox callbacks below, which run outside React's render\n\t// pass. Only `query` needs the synchronous ref above; these three change\n\t// exclusively through a re-render.\n\tconst optionsRef = useLiveRef(options);\n\tconst valueRef = useLiveRef(value);\n\tconst filterRef = useLiveRef(filter);\n\n\t// The framework-free core rather than `useListbox`: `count` and `enabled`\n\t// stay real getters here, exactly as in the source, which is what lets\n\t// `handleInput` write the query and navigate the *new* list in one turn.\n\tconst listbox = useConstant(() =>\n\t\tcreateListbox({\n\t\t\tcount: () =>\n\t\t\t\tcomputeFilteredOptions(\n\t\t\t\t\toptionsRef.current,\n\t\t\t\t\tvalueRef.current,\n\t\t\t\t\tfilterRef.current,\n\t\t\t\t\tqueryRef.current\n\t\t\t\t).length,\n\t\t\tenabled: (i) =>\n\t\t\t\t!computeFilteredOptions(\n\t\t\t\t\toptionsRef.current,\n\t\t\t\t\tvalueRef.current,\n\t\t\t\t\tfilterRef.current,\n\t\t\t\t\tqueryRef.current\n\t\t\t\t)[i]?.disabled,\n\t\t\tloop: true,\n\t\t})\n\t);\n\n\tuseEffect(() => () => listbox.destroy(), [listbox]);\n\n\tconst getActiveIndex = useCallback(() => listbox.activeIndex, [listbox]);\n\tconst activeIndex = useSyncExternalStore(\n\t\tlistbox.subscribe,\n\t\tgetActiveIndex,\n\t\tactiveIndexServerSnapshot\n\t);\n\n\t// Re-syncs the visible text whenever `value` changes for a reason other\n\t// than this component's own `selectOption`/`resolveAndClose` below — those\n\t// already set `query` themselves, synchronously, so in practice this only\n\t// has real work to do when a caller changes `value` from outside. `options`\n\t// is read through a ref (the source's `untrack`) so a caller that recreates\n\t// its options array on every render does not retrigger this and stomp\n\t// whatever the user is mid-typing — only `value` itself does.\n\tuseEffect(() => {\n\t\tsetQuery(findOption(optionsRef.current, value)?.label ?? \"\");\n\t}, [value, optionsRef, setQuery]);\n\n\tconst [inputNode, setInputNode] = useElementRef<HTMLInputElement>();\n\tconst inputRef = useComposedRefs(setInputNode, forwardedRef);\n\n\tconst optionId = useCallback((index: number) => `${panelId}-option-${index}`, [panelId]);\n\n\tfunction openPanel() {\n\t\tif (effectiveDisabled) return;\n\t\tsetOpen(true);\n\t\tconst selectedIndex = filteredOptions.findIndex((o) => o.value === value);\n\t\tlistbox.setActive(selectedIndex >= 0 ? selectedIndex : filteredOptions.length > 0 ? 0 : -1);\n\t}\n\n\t// The single place `value` and `query` change together as a direct\n\t// consequence of the user picking a row.\n\tconst selectOption = useEventCallback((option: ComboboxOption) => {\n\t\tif (effectiveDisabled || option.disabled) return;\n\t\t// Re-picking the row already committed changes nothing — the\n\t\t// changed-only rule every other value-holding component follows\n\t\t// (Select, Tabs, DatePicker, ...), even where, as here, there is no\n\t\t// second cue to fall back on. Read BEFORE the write, and never at the\n\t\t// cost of `onValueChange`, which still fires on a re-pick.\n\t\tconst changed = value !== option.value;\n\t\tsetValueState(option.value);\n\t\tsetQuery(option.label);\n\t\tsetOpen(false);\n\t\tif (changed) playCue(\"select\");\n\t\tonValueChange?.(option.value);\n\t});\n\n\t// A closed set has to resolve on the way out: whatever is left in the\n\t// field either names a real option (already handled by `selectOption`) or\n\t// it doesn't, and in that case the field reverts to the last valid\n\t// selection's label — or clears, when there wasn't one. Escape, an outside\n\t// click (both through the panel's dismissable layer) and a plain blur all\n\t// funnel through this one function, so every way of leaving the field\n\t// agrees on the same outcome.\n\tconst resolveAndClose = useEventCallback(() => {\n\t\tif (!open) return;\n\t\tsetOpen(false);\n\t\tsetQuery(findOption(options, value)?.label ?? \"\");\n\t});\n\n\tfunction handleInput(event: ChangeEvent<HTMLInputElement>) {\n\t\tif (effectiveDisabled) return;\n\t\tsetQuery(event.target.value);\n\t\tsetOpen(true);\n\t\tlistbox.moveToEdge(\"first\");\n\t}\n\n\tfunction handleFocus() {\n\t\topenPanel();\n\t}\n\n\tfunction handleBlur() {\n\t\tresolveAndClose();\n\t}\n\n\tfunction handleKeydown(event: KeyboardEvent<HTMLInputElement>) {\n\t\tif (effectiveDisabled) return;\n\t\tswitch (event.key) {\n\t\t\tcase \"ArrowDown\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tif (!open) {\n\t\t\t\t\topenPanel();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlistbox.move(1);\n\t\t\t\tbreak;\n\t\t\tcase \"ArrowUp\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tif (!open) {\n\t\t\t\t\topenPanel();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlistbox.move(-1);\n\t\t\t\tbreak;\n\t\t\tcase \"Home\":\n\t\t\t\tif (!open) return;\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.moveToEdge(\"first\");\n\t\t\t\tbreak;\n\t\t\tcase \"End\":\n\t\t\t\tif (!open) return;\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.moveToEdge(\"last\");\n\t\t\t\tbreak;\n\t\t\tcase \"Enter\": {\n\t\t\t\tif (!open) return;\n\t\t\t\tconst active = filteredOptions[listbox.activeIndex];\n\t\t\t\tif (active) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tselectOption(active);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst isActive = useCallback((index: number) => index === activeIndex, [activeIndex]);\n\n\tconst context = useMemo<ComboboxContext>(\n\t\t() => ({\n\t\t\topen,\n\t\t\tpanelId,\n\t\t\tinputRef: inputNode,\n\t\t\toptions: filteredOptions,\n\t\t\tquery,\n\t\t\tactiveIndex,\n\t\t\temptyMessage,\n\t\t\toptionId,\n\t\t\tisActive,\n\t\t\tselectOption,\n\t\t\tclose: resolveAndClose,\n\t\t}),\n\t\t[\n\t\t\topen,\n\t\t\tpanelId,\n\t\t\tinputNode,\n\t\t\tfilteredOptions,\n\t\t\tquery,\n\t\t\tactiveIndex,\n\t\t\temptyMessage,\n\t\t\toptionId,\n\t\t\tisActive,\n\t\t\tselectOption,\n\t\t\tresolveAndClose,\n\t\t]\n\t);\n\n\t// A count, not the contents — see the panel's own note on why the list\n\t// itself is never read out loud.\n\tconst resultsMessage =\n\t\tfilteredOptions.length === 1 ? \"1 result\" : `${filteredOptions.length} results`;\n\n\t// Internal handling first, the consumer's listener once after it, with the\n\t// same event — even when the internal handler called `preventDefault`.\n\tfunction onChangeEvent(event: ChangeEvent<HTMLInputElement>) {\n\t\thandleInput(event);\n\t\tonChange?.(event);\n\t}\n\tfunction onFocusEvent(event: FocusEvent<HTMLInputElement>) {\n\t\thandleFocus();\n\t\tonFocus?.(event);\n\t}\n\tfunction onBlurEvent(event: FocusEvent<HTMLInputElement>) {\n\t\thandleBlur();\n\t\tonBlur?.(event);\n\t}\n\tfunction onKeyDownEvent(event: KeyboardEvent<HTMLInputElement>) {\n\t\thandleKeydown(event);\n\t\tonKeyDown?.(event);\n\t}\n\n\t// A consumer `aria-describedby` is merged with the FormField's, never replaces it.\n\tconst describedBy =\n\t\t[field?.describedBy, restProps[\"aria-describedby\"]].filter(Boolean).join(\" \") || undefined;\n\n\tconst classes = cn(\n\t\t\"ft-combobox w-full rounded-[8px] border border-input bg-background px-[12px] py-[9px] text-[13px] text-foreground transition-colors\",\n\t\t\"placeholder:text-muted-foreground\",\n\t\t\"focus-visible:outline-none\",\n\t\t\"disabled:cursor-not-allowed disabled:opacity-50\",\n\t\teffectiveInvalid && \"border-destructive/50\",\n\t\tclassName\n\t);\n\n\t// `children` is omitted from the public props, but a caller outside the types\n\t// can still pass one — an `<input>` is a void element, so it must not reach it.\n\tconst { children: _children, ...inputProps } = restProps as typeof restProps & {\n\t\tchildren?: unknown;\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"ft-combobox-wrapper relative\">\n\t\t\t\t<input\n\t\t\t\t\t{...inputProps}\n\t\t\t\t\tref={inputRef}\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\tid={effectiveId}\n\t\t\t\t\tplaceholder={placeholder}\n\t\t\t\t\tvalue={query}\n\t\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t\trequired={effectiveRequired}\n\t\t\t\t\taria-invalid={effectiveInvalid ? \"true\" : restProps[\"aria-invalid\"]}\n\t\t\t\t\taria-describedby={describedBy}\n\t\t\t\t\taria-label={field?.labelId ? undefined : (label ?? restProps[\"aria-label\"])}\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\taria-haspopup=\"listbox\"\n\t\t\t\t\taria-autocomplete=\"list\"\n\t\t\t\t\taria-controls={open ? panelId : undefined}\n\t\t\t\t\taria-activedescendant={open && activeIndex >= 0 ? optionId(activeIndex) : undefined}\n\t\t\t\t\tclassName={classes}\n\t\t\t\t\tonChange={onChangeEvent}\n\t\t\t\t\tonFocus={onFocusEvent}\n\t\t\t\t\tonBlur={onBlurEvent}\n\t\t\t\t\tonKeyDown={onKeyDownEvent}\n\t\t\t\t/>\n\n\t\t\t\t{/*\n\t\t\t\t\tThe visible input displays the option's *label*, so it cannot also\n\t\t\t\t\tcarry `name` without submitting the label text instead of the actual\n\t\t\t\t\tvalue — this hidden input carries the real submission pair instead,\n\t\t\t\t\tthe same reason a native `<select>` submits its `value` and not its\n\t\t\t\t\tdisplayed text. `required` stays off it (a `type=\"hidden\"` input is\n\t\t\t\t\tbarred from constraint validation in every browser) and lives on the\n\t\t\t\t\tvisible input above instead, where a real validation UI can attach to\n\t\t\t\t\tit.\n\n\t\t\t\t\tNo `readOnly` and no no-op `onChange`: React's controlled-value\n\t\t\t\t\twarning exempts `type=\"hidden\"`, which is the one input kind with\n\t\t\t\t\tno writer by construction — so the DOM stays exactly what the\n\t\t\t\t\tsource emits.\n\t\t\t\t*/}\n\t\t\t\t{name ? (\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"hidden\"\n\t\t\t\t\t\tname={name}\n\t\t\t\t\t\tvalue={value}\n\t\t\t\t\t\tform={restProps.form}\n\t\t\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\n\t\t\t\t{/*\n\t\t\t\t\tAlways mounted, whether or not the panel is open — an element that\n\t\t\t\t\tonly appears at the same moment its own text does usually goes\n\t\t\t\t\tunannounced. Content is a count, never the option list itself, so\n\t\t\t\t\tfast typing doesn't turn into a screen reader narrating every\n\t\t\t\t\tkeystroke's worth of rows.\n\t\t\t\t*/}\n\t\t\t\t<div className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n\t\t\t\t\t{open ? resultsMessage : \"\"}\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{/*\n\t\t\t\tThe panel is rendered UNCONDITIONALLY, where the source wraps it in\n\t\t\t\t`{#if open}` — see this folder's README. `ComboboxPanel` owns the\n\t\t\t\t`usePresence` gate itself, and its `Portal` has to be mounted before\n\t\t\t\tthe commit that opens the panel or the entrance leg is silently\n\t\t\t\tskipped.\n\t\t\t*/}\n\t\t\t<COMBOBOX_KEY.Provider value={context}>\n\t\t\t\t<ComboboxPanel />\n\t\t\t</COMBOBOX_KEY.Provider>\n\t\t</>\n\t);\n});\n\nCombobox.displayName = \"Combobox\";\n","type":"registry:ui","target":"@ui/fancy/components/combobox/Combobox.tsx"},{"path":"react/src/components/combobox/ComboboxPanel.tsx","content":"\"use client\";\n\nimport { Fragment, useRef } from \"react\";\nimport type { MouseEvent } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { Portal } from \"../../internals/Portal\";\nimport { useDismissable } from \"../../internals/dismissable\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useAnchorPosition } from \"../../internals/use-anchor-position\";\nimport { anchored, originFor } from \"../../internals/motion/anchored\";\nimport { usePresence } from \"../../internals/motion/presence\";\nimport { getMatchRange } from \"./match\";\nimport { COMBOBOX_KEY } from \"./types\";\n\n/**\n * The portalled listbox panel.\n *\n * A `<div role=\"listbox\">`, not a `<ul>` — its rows are real `<button>`s\n * (role=\"option\" overrides the native semantics for assistive tech), and a\n * `<button>` is not a permitted child of `<ul>`.\n *\n * No focus trap: focus never leaves the input for this pattern\n * (`aria-activedescendant` tracks the active row instead), so there is\n * nothing to move focus into or return it from. Each row is a real button so\n * it stays natively keyboard-operable and the a11y linter is right to ask\n * for one, but `tabindex=\"-1\"` keeps it out of the tab sequence — the input\n * is the only stop — and `onMouseDown` calls `preventDefault()` so the\n * pointer's own focus-on-mousedown never fires before the row's own\n * `onClick` runs: without it, clicking a row would blur the input a beat\n * before selecting anything.\n *\n * It arrives and leaves on ONE bidirectional transition, never a split\n * in/out pair: one leg handed the in-flight counterpart's current position\n * means a panel reopened mid-exit continues from where it is instead of\n * snapping to invisible first. `entering` is what tells it which way it is\n * going, and it comes from the presence clock rather than the transition's\n * own direction.\n */\nexport function ComboboxPanel() {\n\t// `Combobox` only ever renders this component inside its own provider, so\n\t// the context is always present by the time this runs — no\n\t// standalone-usage fallback to design for, which is what `useRequired`\n\t// encodes.\n\tconst ctx = COMBOBOX_KEY.useRequired();\n\n\t// Convention C-1: the NODE, not a ref. The panel is created by\n\t// `presence.mounted`, so a `useRef` + `[]`-deps effect would still be\n\t// holding `null` when the anchor and dismiss effects fire.\n\tconst [panel, setPanelNode] = useElementRef<HTMLDivElement>();\n\n\t// The side and cross-axis alignment the panel was ACTUALLY placed on.\n\t// This panel always asks for `bottom`/`start`, but a combobox sitting low\n\t// in the viewport flips to `top` routinely, and clamping near a viewport\n\t// edge slides it along the cross axis — the entrance origin has to follow\n\t// both, or the panel grows out of a corner that is no longer the one\n\t// touching the input. Seeded with the requested values, so the un-flipped\n\t// case never depends on a first placement callback having fired.\n\tconst { side: resolvedSide, align: resolvedAlign } = useAnchorPosition(panel, {\n\t\tanchor: ctx.inputRef,\n\t\tside: \"bottom\",\n\t\talign: \"start\",\n\t\toffset: 4,\n\t});\n\n\tconst presence = usePresence(ctx.open);\n\n\tuseDismissable(panel, {\n\t\tonDismiss: ctx.close,\n\t\texclude: [ctx.inputRef],\n\t\tactive: ctx.open,\n\t});\n\n\t// Convention C-2: composed ABOVE the mounted gate below. Calling this\n\t// inside the JSX branch would be a conditional hook and would throw the\n\t// first time `mounted` flips.\n\tconst panelRef = useComposedRefs(\n\t\tsetPanelNode,\n\t\tpresence.register(anchored, (entering) => ({ side: resolvedSide, entering }))\n\t);\n\n\t// Svelte destroys the `{#if open}` branch that owns this component, and\n\t// marks it INERT before playing the outro — its scheduler skips inert\n\t// effects, so the rows on screen during the exit are frozen at whatever\n\t// they were the instant the close began. React re-renders an exiting\n\t// subtree normally, so the freeze is explicit here. It is load-bearing for\n\t// this component in particular: `close()` resolves the query back to the\n\t// selected option's label in the very turn it flips `open`, and that\n\t// re-filters the list — so a panel that closed while showing its empty\n\t// message would repopulate with every option halfway through its own fade.\n\tconst frozen = useRef({\n\t\toptions: ctx.options,\n\t\tquery: ctx.query,\n\t\tactiveIndex: ctx.activeIndex,\n\t});\n\tif (ctx.open) {\n\t\tfrozen.current = { options: ctx.options, query: ctx.query, activeIndex: ctx.activeIndex };\n\t}\n\tconst view = frozen.current;\n\n\t// The `Portal` stays mounted and its CHILDREN are what `presence.mounted`\n\t// gates. `usePortalTarget` resolves its container in a layout effect, so a\n\t// `Portal` first mounting in the same commit as the panel renders nothing\n\t// on that pass — the registered node would then not exist when\n\t// `usePresence`'s own layout effect looks for legs to start, and the\n\t// entrance would be skipped outright while every other assertion still\n\t// passed.\n\treturn (\n\t\t<Portal>\n\t\t\t{presence.mounted ? (\n\t\t\t\t<div\n\t\t\t\t\tref={panelRef}\n\t\t\t\t\tid={ctx.panelId}\n\t\t\t\t\trole=\"listbox\"\n\t\t\t\t\tclassName=\"ft-combobox-panel border-border bg-popover text-popover-foreground flex max-h-[260px] w-max min-w-[220px] flex-col gap-[1px] overflow-auto rounded-[10px] border p-[5px] text-[13px] shadow-lg outline-none\"\n\t\t\t\t\tdata-state={presence.surfaceState}\n\t\t\t\t\tdata-side={resolvedSide}\n\t\t\t\t\tdata-align=\"start\"\n\t\t\t\t\tstyle={{ transformOrigin: originFor(resolvedSide, resolvedAlign) }}\n\t\t\t\t>\n\t\t\t\t\t{view.options.length === 0 ? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t\tclassName=\"ft-combobox-empty text-muted-foreground rounded-[6px] px-[10px] py-[7px]\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{ctx.emptyMessage}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : (\n\t\t\t\t\t\tview.options.map((option, index) => {\n\t\t\t\t\t\t\tconst range = getMatchRange(option.label, view.query);\n\t\t\t\t\t\t\tconst isActive = index === view.activeIndex;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t\t\t`onMouseDown` below defends against a real browser's\n\t\t\t\t\t\t\t\t\tfocus-follows-mousedown default action stealing focus onto this\n\t\t\t\t\t\t\t\t\trow before its own `onClick` commits — without it, that focus\n\t\t\t\t\t\t\t\t\tshift blurs the input first, which closes the panel (unmounting\n\t\t\t\t\t\t\t\t\tthis row) before the click already in flight can land. jsdom\n\t\t\t\t\t\t\t\t\timplements no such default action to suppress, so no test in this\n\t\t\t\t\t\t\t\t\trepo can watch this guard prevent that outcome — only that the\n\t\t\t\t\t\t\t\t\tcall happens (`Combobox.test.tsx`'s \"the row's mousedown handler\n\t\t\t\t\t\t\t\t\tcalls preventDefault\" dispatches a bare `mousedown` and checks\n\t\t\t\t\t\t\t\t\t`event.defaultPrevented`). Do not delete this as dead code on the\n\t\t\t\t\t\t\t\t\tstrength of a green suite.\n\t\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\tkey={option.value}\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tid={ctx.optionId(index)}\n\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\tdisabled={option.disabled}\n\t\t\t\t\t\t\t\t\taria-selected={isActive}\n\t\t\t\t\t\t\t\t\taria-disabled={option.disabled ? \"true\" : undefined}\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\"ft-combobox-option w-full cursor-pointer rounded-[6px] px-[10px] py-[7px] text-left\",\n\t\t\t\t\t\t\t\t\t\tisActive && !option.disabled && \"bg-accent text-accent-foreground\",\n\t\t\t\t\t\t\t\t\t\toption.disabled && \"pointer-events-none opacity-50\"\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\tonMouseDown={(event: MouseEvent<HTMLButtonElement>) => event.preventDefault()}\n\t\t\t\t\t\t\t\t\tonClick={() => ctx.selectOption(option)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{range ? (\n\t\t\t\t\t\t\t\t\t\t<Fragment>\n\t\t\t\t\t\t\t\t\t\t\t{option.label.slice(0, range.start)}\n\t\t\t\t\t\t\t\t\t\t\t<strong>{option.label.slice(range.start, range.end)}</strong>\n\t\t\t\t\t\t\t\t\t\t\t{option.label.slice(range.end)}\n\t\t\t\t\t\t\t\t\t\t</Fragment>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\toption.label\n\t\t\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);\n\t\t\t\t\t\t})\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</Portal>\n\t);\n}\n\nComboboxPanel.displayName = \"ComboboxPanel\";\n","type":"registry:ui","target":"@ui/fancy/components/combobox/ComboboxPanel.tsx"},{"path":"react/src/components/combobox/combobox.css","content":"/*\n * The Svelte source's <style> block, anchored on the root class it already\n * named itself (`.ft-combobox`, the first token of the input's own `cn()`\n * call) — no port-added anchor was needed.\n *\n * The `--ft-accent` fallback literals are re-typed here by hand, per\n * PORTING.md's custom-property rule: the component must not depend on an\n * app-level token existing.\n */\n.ft-combobox:focus-visible {\n\t--ft-field-accent: var(\n\t\t--ft-accent,\n\t\tlight-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))\n\t);\n\tborder-color: var(--ft-field-accent);\n\tbox-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-field-accent) 25%, transparent);\n}\n","type":"registry:ui","target":"@ui/fancy/components/combobox/combobox.css"},{"path":"react/src/components/combobox/index.ts","content":"\"use client\";\n\nexport { Combobox } from \"./Combobox\";\nexport type { ComboboxProps, ComboboxOption } from \"./Combobox\";\nexport { COMBOBOX_KEY } from \"./types\";\nexport type { ComboboxContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/combobox/index.ts"},{"path":"react/src/components/combobox/match.ts","content":"\"use client\";\n\n// Pure text-matching helpers shared by the root and its floating panel.\n//\n// `defaultFilter` and `getMatchRange` are two independent concerns that\n// happen to agree by construction: the default filter is \"does the label\n// contain the query, case-insensitively\", and `getMatchRange` finds exactly\n// that same substring. When a caller supplies a custom `filter` prop, the two\n// deliberately keep working independently — `getMatchRange` still only ever\n// looks for a literal case-insensitive substring, regardless of what logic\n// actually selected the option, so the span it reports is always true (a\n// real occurrence of the query text in the label) even when it cannot\n// explain *why* the custom filter chose this option. No literal substring\n// found means no highlight, rather than a naive `label.indexOf(query)` (case\n// sensitive, unnormalized) that would either miss a case-insensitive match\n// entirely or, worse, highlight a coincidental span a custom filter had\n// nothing to do with.\n\nexport function defaultFilter(label: string, query: string): boolean {\n\treturn label.toLowerCase().includes(query.toLowerCase());\n}\n\nexport interface MatchRange {\n\tstart: number;\n\tend: number;\n}\n\n/**\n * Finds the first case-insensitive occurrence of `query` inside `label`.\n *\n * Returns `null` when `query` is empty or not literally present in `label`\n * — including when a custom `filter` matched the option for a reason\n * unrelated to a literal substring (fuzzy matching, matching a different\n * field entirely). Callers render the label plain in that case instead of\n * fabricating a highlighted span.\n */\nexport function getMatchRange(label: string, query: string): MatchRange | null {\n\tif (!query) return null;\n\tconst index = label.toLowerCase().indexOf(query.toLowerCase());\n\tif (index === -1) return null;\n\treturn { start: index, end: index + query.length };\n}\n","type":"registry:ui","target":"@ui/fancy/components/combobox/match.ts"},{"path":"react/src/components/combobox/types.ts","content":"\"use client\";\n\n/**\n * The contract between the Combobox root and its portalled listbox panel.\n *\n * Combobox is a **closed set**: `options` is the only vocabulary of valid\n * values, and the root owns everything that decides which of them are\n * currently visible, which is active for keyboard navigation, and how a\n * click or Enter on a row resolves back into `value`. The panel only ever\n * reads this and calls back into it — it never touches `value` or `query`\n * directly, the same division a popover context draws between a root and its\n * content.\n */\n\nimport { createInternalContext } from \"../../internals/dom/context\";\n\n/** One selectable option. `value` is what `Combobox` reads and writes; `label` is what a person sees and types against. */\nexport interface ComboboxOption {\n\tvalue: string;\n\tlabel: string;\n\tdisabled?: boolean;\n}\n\nexport interface ComboboxContext {\n\t/** Whether the panel is open. Read by the panel's own exit transition to\n\t *  tell an entrance from a departure — one bidirectional transition\n\t *  cannot distinguish them on its own. Also the panel's dismissable\n\t *  `active` gate: the instant this flips false the layer stops answering\n\t *  Escape, so a second Escape during the fade reaches whatever is\n\t *  underneath instead of being swallowed by a panel that is already\n\t *  leaving. */\n\treadonly open: boolean;\n\t/** The panel's own id — also what the input's `aria-controls` points at while open. */\n\treadonly panelId: string;\n\t/** The real input element, once mounted — what the panel anchors against and excludes from outside-click dismissal. */\n\treadonly inputRef: HTMLInputElement | null;\n\t/** The options currently visible in the panel, already filtered by the current query. */\n\treadonly options: ComboboxOption[];\n\t/** The current query text, for computing each row's highlighted span. */\n\treadonly query: string;\n\t/** Index of the keyboard-active row within `options`, or -1 when none is active. */\n\treadonly activeIndex: number;\n\t/** Shown in place of the option list when `options` is empty. */\n\treadonly emptyMessage: string;\n\t/** The dom id a row at this index must carry, so `aria-activedescendant` can point at it. */\n\toptionId(index: number): string;\n\t/** Whether the row at this index is the keyboard-active one. */\n\tisActive(index: number): boolean;\n\t/** Commits `option` as the selection and closes the panel. No-ops for a disabled option. */\n\tselectOption(option: ComboboxOption): void;\n\t/** Closes the panel, resolving the query back to the current value's label (or clearing it). Routes Escape, outside click, and blur through the same rule. */\n\tclose(): void;\n}\n\n/**\n * The context the root publishes to its panel.\n *\n * The Svelte source keys this on a `unique symbol` passed to\n * `setContext`/`getContext`; React's own context object plays that role here,\n * under the same name, because the Svelte `index.ts` exports the key — so it\n * keeps its public identity on both sides.\n *\n * Read it with `COMBOBOX_KEY.useRequired()`: the panel is only ever rendered\n * by the root, so a missing provider is a programming error rather than a\n * degrade-gracefully case.\n */\nexport const COMBOBOX_KEY = createInternalContext<ComboboxContext>(\"COMBOBOX_KEY\");\n","type":"registry:ui","target":"@ui/fancy/components/combobox/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/combobox?framework=react"}}