{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"select","type":"registry:ui","title":"Select","description":"A single-choice dropdown built on a real button and a portalled listbox panel, with full keyboard navigation, typeahead and focus that never leaves the trigger.","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/select/Select.tsx","content":"\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n\tComponentPropsWithoutRef,\n\tKeyboardEvent as ReactKeyboardEvent,\n\tMouseEvent as ReactMouseEvent,\n} from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useField } from \"../../internals/field\";\nimport { useListbox } from \"../../internals/listbox\";\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 { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport type { Side, Align } from \"../../internals/anchor-position\";\nimport { SelectPanel } from \"./SelectPanel\";\nimport { SelectReactContext } from \"./types\";\nimport type { SelectContext, SelectOption } from \"./types\";\nimport \"./select.css\";\n\nexport type { SelectOption };\n\n/**\n * Every other native `<button>` attribute and listener (`aria-*`, `data-*`,\n * `onFocus`, `onBlur`, `onKeyDown`, `form`, ...) is forwarded to the trigger.\n * The managed ARIA (`role`, `aria-expanded`, `aria-controls`,\n * `aria-activedescendant`, `aria-haspopup`, `aria-invalid`, `aria-required`)\n * stays the component's own.\n */\nexport interface SelectProps extends Omit<\n\tComponentPropsWithoutRef<\"button\">,\n\t| \"className\"\n\t| \"id\"\n\t| \"name\"\n\t| \"value\"\n\t| \"disabled\"\n\t| \"type\"\n\t| \"role\"\n\t| \"children\"\n\t| \"placeholder\"\n\t| \"onChange\"\n> {\n\t/** The options to choose from, in order. */\n\toptions: SelectOption[];\n\t/** The selected value. `\"\"` means nothing is selected. */\n\tvalue?: string;\n\t/** Called with the new value whenever the selection changes. */\n\tonValueChange?: (value: string) => void;\n\t/** Shown in the trigger while nothing is selected. */\n\tplaceholder?: string;\n\t/** Blocks opening and excludes the control from form submission. Overridden by a surrounding FormField. */\n\tdisabled?: boolean;\n\t/** Marks the control 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`. When set, a hidden input carries the value so the control participates in form submission. */\n\tname?: string;\n\t/** Accessible name — for a control with no visible Label next to it. */\n\tlabel?: string;\n\t/** Side of the trigger to place the panel on. */\n\tside?: Side;\n\t/** Alignment along the trigger's cross axis. */\n\talign?: Align;\n\t/** Additional CSS classes, merged onto the trigger. */\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\n/**\n * A combobox trigger plus a portalled listbox panel, driven off an `options`\n * array.\n *\n * `forwardRef` because the source declares `ref = $bindable(null)` on the\n * trigger button — the exposed element is the `<button>`, not a wrapper.\n */\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\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\tside = \"bottom\",\n\t\talign = \"start\",\n\t\tclassName,\n\t\tsound = false,\n\t\tonClick,\n\t\tonKeyDown,\n\t\t...restProps\n\t},\n\tforwardedRef\n) {\n\t// Undefined outside a FormField — every value below then falls back to\n\t// this component's own props instead of the context, so the control works\n\t// standalone exactly as it does wrapped. The root is a real `<button>`, a\n\t// labelable element, so a wrapping FormField's own `<Label htmlFor>`\n\t// targets `controlId` directly — there is no `labelId`/`aria-labelledby`\n\t// path to wire here the way a `role=\"radiogroup\"` root would need.\n\tconst field = useField();\n\n\tconst effectiveId = field?.controlId ?? id;\n\tconst effectiveDisabled = field?.disabled ?? disabled;\n\tconst effectiveRequired = field?.required ?? required;\n\tconst effectiveInvalid = field?.invalid ?? invalid;\n\n\t// `useFancyId()`, the counterpart of the source's `$props.id()`: this seeds\n\t// the panel and option ids, which must already agree with themselves on\n\t// the very first server-rendered paint (the closed trigger renders with no\n\t// `aria-controls` at all, but the ids it would point to once open still\n\t// have to be stable across hydration, not generated fresh client-side).\n\tconst uid = useFancyId();\n\tconst panelId = `${uid}-listbox`;\n\tconst optionId = useCallback((index: number) => `${uid}-option-${index}`, [uid]);\n\n\tconst playCue = useSoundCue(sound);\n\n\t// `open` is state AND a ref written in the same two functions. The ref is\n\t// not a cache: `openPanel` flips `open` and then, synchronously, moves the\n\t// listbox's active index — and `onActiveChange` below branches on whether\n\t// the panel is open. The source reads `$state` there and sees the value it\n\t// just wrote; a React render value would still be the pre-handler `false`,\n\t// and opening with ArrowDown would silently COMMIT the first option\n\t// instead of merely highlighting it.\n\tconst [open, setOpenState] = useState(false);\n\tconst openRef = useRef(false);\n\tconst setOpen = useCallback((next: boolean) => {\n\t\topenRef.current = next;\n\t\tsetOpenState(next);\n\t}, []);\n\n\t// Convention C-1: the node, not a ref — the panel anchors against it and\n\t// the dismiss layer excludes it, and both are hooks keyed on the node.\n\tconst [triggerNode, setTriggerNode] = useElementRef<HTMLButtonElement>();\n\tconst triggerRef = useComposedRefs(setTriggerNode, forwardedRef);\n\n\t// A plain ref: nothing renders off it. It exists so the active row can be\n\t// scrolled into view, which is DOM work, never a render input.\n\tconst panelRef = useRef<HTMLDivElement | null>(null);\n\n\t// The React shape of the source's `value = $bindable(\"\")`: an internal\n\t// copy seeded from the prop, re-synced during render whenever the CALLER\n\t// changes the prop, and free to move on its own in between. That is what\n\t// makes all three documented call shapes work off one implementation — a\n\t// caller driving `value` from its own state, a caller who passes only\n\t// `onValueChange`, and a caller who passes neither.\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\tconst selectedIndex = options.findIndex((o) => o.value === value);\n\tconst selectedOption = selectedIndex === -1 ? undefined : options[selectedIndex];\n\n\tfunction isOptionEnabled(index: number): boolean {\n\t\treturn !options[index]?.disabled;\n\t}\n\n\t// The one place `value` changes, in either direction — a plain function,\n\t// never an effect, so it can never read and write `value` in the same pass\n\t// and never fights a caller's own controlled write.\n\t/** Returns true when the value actually changed (and a `select` cue played). */\n\tfunction setValue(next: string): boolean {\n\t\tif (value === next) return false;\n\t\tsetValueState(next);\n\t\tplayCue(\"select\");\n\t\tonValueChange?.(next);\n\t\treturn true;\n\t}\n\n\tfunction commitIndex(index: number): boolean {\n\t\tconst option = options[index];\n\t\tif (!option || option.disabled) return false;\n\t\treturn setValue(option.value);\n\t}\n\n\t// The single hook the listbox core calls whenever the active index moves,\n\t// for any reason (arrow keys, Home/End, a pointer hover, typeahead). What\n\t// \"active\" means depends entirely on whether the panel is open: while\n\t// open it is only a highlight, scrolled into view but never written to\n\t// `value` until something commits it (Enter, Tab, a click). While closed,\n\t// there is no highlight to show — the only way this callback fires at all\n\t// is closed-state typeahead, and a native `<select>` commits that\n\t// immediately rather than merely remembering it, so this does too.\n\tconst handleActiveChange = useEventCallback((index: number) => {\n\t\tif (!openRef.current) {\n\t\t\tcommitIndex(index);\n\t\t\treturn;\n\t\t}\n\t\tconst row = panelRef.current?.querySelector<HTMLElement>(`#${CSS.escape(optionId(index))}`);\n\t\trow?.scrollIntoView?.({ block: \"nearest\" });\n\t});\n\n\tconst listbox = useListbox({\n\t\tcount: options.length,\n\t\tenabled: isOptionEnabled,\n\t\tonActiveChange: handleActiveChange,\n\t});\n\n\t// If `options` shrinks while the panel is open, a previously-valid\n\t// `activeIndex` can end up pointing past the end of the new, shorter\n\t// array. Nothing else re-checks this: the listbox module only reacts to\n\t// explicit move/typeahead/setActive calls, not to the option count\n\t// changing out from under it between them. Left uncorrected,\n\t// `aria-activedescendant` below would keep citing an option id with no\n\t// matching row left in the DOM — the same \"attribute pointing at nothing\"\n\t// failure ruled out for `aria-controls` while closed.\n\tuseEffect(() => {\n\t\tif (listbox.activeIndex !== -1 && listbox.activeIndex >= options.length) {\n\t\t\tlistbox.setActive(-1);\n\t\t}\n\t}, [listbox, options.length]);\n\n\tfunction openPanel(fallbackEdge: \"first\" | \"last\"): void {\n\t\tif (effectiveDisabled) return;\n\t\tsetOpen(true);\n\t\tplayCue(\"open\");\n\t\tif (selectedIndex !== -1 && isOptionEnabled(selectedIndex)) {\n\t\t\tlistbox.setActive(selectedIndex);\n\t\t} else {\n\t\t\tlistbox.moveToEdge(fallbackEdge);\n\t\t}\n\t}\n\n\t// `reason` distinguishes a commit-flavoured close (a value was just\n\t// picked — by click, Enter/Space, Tab, or closed-state typeahead) from a\n\t// plain dismiss (Escape, an outside click, or the trigger toggling the\n\t// panel shut with nothing highlighted). Only a dismiss plays the `close`\n\t// cue — a commit already played `select` inside `setValue`/`commitIndex`\n\t// above, and the contract is one cue per interaction, never both.\n\tfunction closePanel(reason: \"commit\" | \"dismiss\" = \"dismiss\"): void {\n\t\tsetOpen(false);\n\t\tif (reason === \"dismiss\") playCue(\"close\");\n\t}\n\n\tfunction commitActiveAndClose(): void {\n\t\t// The close reason follows the ACTUAL outcome: re-committing the value\n\t\t// that is already selected changes nothing, so it closes like a dismiss\n\t\t// and is not swallowed into silence.\n\t\tconst committed = listbox.activeIndex !== -1 && commitIndex(listbox.activeIndex);\n\t\tclosePanel(committed ? \"commit\" : \"dismiss\");\n\t}\n\n\tfunction handleTriggerClick(): void {\n\t\tif (effectiveDisabled) return;\n\t\tif (open) {\n\t\t\tclosePanel();\n\t\t} else {\n\t\t\topenPanel(\"first\");\n\t\t}\n\t}\n\n\tfunction labelAt(index: number): string {\n\t\treturn options[index]?.label ?? \"\";\n\t}\n\n\t// A single, non-modified character — Space excluded, since it is handled\n\t// as its own key below (both \"open\" and \"commit\", depending on `open`).\n\tfunction isTypeaheadKey(event: ReactKeyboardEvent): boolean {\n\t\treturn (\n\t\t\tevent.key.length === 1 &&\n\t\t\tevent.key !== \" \" &&\n\t\t\t!event.ctrlKey &&\n\t\t\t!event.metaKey &&\n\t\t\t!event.altKey\n\t\t);\n\t}\n\n\t// No Escape handling here on purpose — `SelectPanel`'s own dismiss layer\n\t// already closes on Escape (and an outside click) via the document-level\n\t// listener it owns; a second listener here would be a bug, not a backstop.\n\t// Because Escape only ever closes and closing never itself writes `value`,\n\t// \"Escape closes without changing the value\" falls out for free: nothing\n\t// in this component's own keydown handling below writes `value` for any\n\t// key other than Enter/Space/Tab.\n\tfunction handleTriggerKeydown(event: ReactKeyboardEvent<HTMLButtonElement>): void {\n\t\tif (effectiveDisabled) return;\n\n\t\tif (!open) {\n\t\t\tswitch (event.key) {\n\t\t\t\tcase \"Enter\":\n\t\t\t\tcase \" \":\n\t\t\t\tcase \"ArrowDown\":\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\topenPanel(\"first\");\n\t\t\t\t\treturn;\n\t\t\t\tcase \"ArrowUp\":\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\topenPanel(\"last\");\n\t\t\t\t\treturn;\n\t\t\t\tdefault:\n\t\t\t\t\t// Typing while closed selects by typeahead without opening —\n\t\t\t\t\t// what a native <select> does. `handleActiveChange` above is\n\t\t\t\t\t// what turns the resulting highlight into a real commit while\n\t\t\t\t\t// `open` is false.\n\t\t\t\t\tif (isTypeaheadKey(event)) listbox.typeahead(event.key, labelAt);\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tswitch (event.key) {\n\t\t\tcase \"ArrowDown\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.move(1);\n\t\t\t\treturn;\n\t\t\tcase \"ArrowUp\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.move(-1);\n\t\t\t\treturn;\n\t\t\tcase \"Home\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.moveToEdge(\"first\");\n\t\t\t\treturn;\n\t\t\tcase \"End\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.moveToEdge(\"last\");\n\t\t\t\treturn;\n\t\t\tcase \"Enter\":\n\t\t\tcase \" \":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tcommitActiveAndClose();\n\t\t\t\treturn;\n\t\t\tcase \"Tab\":\n\t\t\t\t// Chosen behaviour (documented in the README): Tab commits the\n\t\t\t\t// highlighted option, exactly like Enter, but is never\n\t\t\t\t// prevented — focus still moves on to the next control the\n\t\t\t\t// browser would have picked anyway. The alternative (close\n\t\t\t\t// without committing) would silently discard a highlight the\n\t\t\t\t// user very likely meant to pick, for no benefit over this one.\n\t\t\t\tcommitActiveAndClose();\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\tif (isTypeaheadKey(event)) listbox.typeahead(event.key, labelAt);\n\t\t\t\treturn;\n\t\t}\n\t}\n\n\t// Identity-stable wrappers, so the context object below rebuilds only when\n\t// something a consumer RENDERS actually changed.\n\tconst contextSetActive = useEventCallback((index: number) => {\n\t\tlistbox.setActive(index);\n\t});\n\tconst contextCommit = useEventCallback((index: number) => {\n\t\tconst committed = commitIndex(index);\n\t\tclosePanel(committed ? \"commit\" : \"dismiss\");\n\t});\n\t// Wrapped so a caller passing an event object can never leak it in as `reason`.\n\tconst contextClose = useEventCallback(() => closePanel(\"dismiss\"));\n\n\tconst activeIndex = listbox.activeIndex;\n\tconst context = useMemo<SelectContext>(\n\t\t() => ({\n\t\t\topen,\n\t\t\tpanelId,\n\t\t\toptions,\n\t\t\tvalue,\n\t\t\tlabel,\n\t\t\tactiveIndex,\n\t\t\tside,\n\t\t\talign,\n\t\t\ttriggerRef: triggerNode,\n\t\t\toptionId,\n\t\t\tisSelected: (index: number) => index === selectedIndex,\n\t\t\tisActive: (index: number) => index === activeIndex,\n\t\t\tsetActive: contextSetActive,\n\t\t\tcommit: contextCommit,\n\t\t\tclose: contextClose,\n\t\t}),\n\t\t[\n\t\t\topen,\n\t\t\tpanelId,\n\t\t\toptions,\n\t\t\tvalue,\n\t\t\tlabel,\n\t\t\tactiveIndex,\n\t\t\tside,\n\t\t\talign,\n\t\t\ttriggerNode,\n\t\t\toptionId,\n\t\t\tselectedIndex,\n\t\t\tcontextSetActive,\n\t\t\tcontextCommit,\n\t\t\tcontextClose,\n\t\t]\n\t);\n\n\t// Internal handling first, the consumer's listener once after it. A disabled\n\t// control runs neither (a disabled <button> would not fire them anyway).\n\tfunction handleClick(event: ReactMouseEvent<HTMLButtonElement>): void {\n\t\tif (effectiveDisabled) return;\n\t\thandleTriggerClick();\n\t\tonClick?.(event);\n\t}\n\n\tfunction handleKeyDown(event: ReactKeyboardEvent<HTMLButtonElement>): void {\n\t\tif (effectiveDisabled) return;\n\t\thandleTriggerKeydown(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-select-trigger flex w-full items-center justify-between gap-2 rounded-[8px] border border-input bg-background px-[12px] py-[9px] text-left text-[13px] text-foreground transition-colors\",\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\treturn (\n\t\t<SelectReactContext.Provider value={context}>\n\t\t\t<button\n\t\t\t\t{...restProps}\n\t\t\t\tref={triggerRef}\n\t\t\t\tid={effectiveId}\n\t\t\t\ttype=\"button\"\n\t\t\t\trole=\"combobox\"\n\t\t\t\taria-haspopup=\"listbox\"\n\t\t\t\taria-expanded={open}\n\t\t\t\taria-controls={open ? panelId : undefined}\n\t\t\t\taria-activedescendant={open && activeIndex !== -1 ? optionId(activeIndex) : undefined}\n\t\t\t\taria-invalid={effectiveInvalid ? \"true\" : restProps[\"aria-invalid\"]}\n\t\t\t\taria-required={effectiveRequired ? \"true\" : restProps[\"aria-required\"]}\n\t\t\t\taria-describedby={describedBy}\n\t\t\t\taria-label={label ?? restProps[\"aria-label\"]}\n\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\tclassName={classes}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t>\n\t\t\t\t<span className={cn(\"truncate\", !selectedOption && \"text-muted-foreground\")}>\n\t\t\t\t\t{selectedOption?.label ?? placeholder ?? \"\"}\n\t\t\t\t</span>\n\t\t\t\t<span aria-hidden=\"true\" className=\"ft-select-caret text-muted-foreground text-[9px]\">\n\t\t\t\t\t▼\n\t\t\t\t</span>\n\t\t\t</button>\n\n\t\t\t{name ? (\n\t\t\t\t<input\n\t\t\t\t\ttype=\"hidden\"\n\t\t\t\t\tname={name}\n\t\t\t\t\tvalue={value}\n\t\t\t\t\tform={restProps.form}\n\t\t\t\t\tdisabled={effectiveDisabled}\n\t\t\t\t/>\n\t\t\t) : null}\n\n\t\t\t{/*\n\t\t\t\tRendered unconditionally, with the mount gate inside — the React\n\t\t\t\tshape of the source's `{#if open}`. The panel has to survive\n\t\t\t\t`open` flipping false for the length of its own exit, and its\n\t\t\t\tportal target has to be resolved before the commit that opens it.\n\t\t\t*/}\n\t\t\t<SelectPanel ref={panelRef} />\n\t\t</SelectReactContext.Provider>\n\t);\n});\n\nSelect.displayName = \"Select\";\n","type":"registry:ui","target":"@ui/fancy/components/select/Select.tsx"},{"path":"react/src/components/select/SelectPanel.tsx","content":"\"use client\";\n\nimport { forwardRef, useRef } from \"react\";\nimport type { MouseEvent as ReactMouseEvent } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { Portal } from \"../../internals/Portal\";\nimport { useAnchorPosition } from \"../../internals/use-anchor-position\";\nimport { useDismissable } from \"../../internals/dismissable\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { anchored, originFor } from \"../../internals/motion/anchored\";\nimport { usePresence } from \"../../internals/motion/presence\";\nimport { useSelectContext } from \"./types\";\nimport type { SelectOption } from \"./types\";\nimport \"./select-panel.css\";\n\nexport interface SelectPanelProps {\n\t/** Additional CSS classes, merged onto the panel. */\n\tclassName?: string;\n}\n\n/**\n * `Select`'s portalled listbox.\n *\n * No focus trap here, unlike a dialog surface — a combobox listbox is not a\n * dialog. Focus deliberately never leaves the trigger; the active row is only\n * ever communicated through `aria-activedescendant` on the button, which is\n * why this panel carries `role=\"listbox\"` and `role=\"option\"` rows instead of\n * a plain menu. Focus staying there through a mouse click specifically depends\n * on `preventFocusSteal` below, not merely on this component never calling\n * `.focus()` — see its own comment.\n *\n * ONE bidirectional transition, never a split in/out pair: `usePresence` owns\n * both directions on one clock, so a panel reopened mid-exit continues from\n * where it is instead of snapping to invisible first. `entering` is what tells\n * the transition which way it is going, and it arrives from the presence\n * handle rather than being guessed from a direction the runtime reports as\n * `\"both\"`.\n *\n * `Select` renders this component unconditionally and the mount gate lives\n * here — the React shape of the source's `{#if open}` one level up, which is\n * also why `open` still has to arrive through the context. `data-state` is an\n * ORDINARY attribute (divergence D-2): the source writes it imperatively from\n * a transition event because its scheduler skips effects inside a closing\n * branch, and React re-renders an exiting surface normally. `inert` is never\n * written by hand either: `usePresence` sets it on the registered node for the\n * whole exit, which is exactly what a closing listbox wants — the rows stop\n * taking clicks the instant the panel starts leaving.\n */\nexport const SelectPanel = forwardRef<HTMLDivElement, SelectPanelProps>(function SelectPanel(\n\t{ className },\n\tforwardedRef\n) {\n\t// `Select` only ever renders this component beneath its own provider, so\n\t// the context is always present by the time this runs.\n\tconst ctx = useSelectContext();\n\n\t// Convention C-1: the NODE, not a ref. The panel element is created by\n\t// `presence.mounted`, so a `useRef` + `[]`-deps effect would still be\n\t// holding `null` when the anchoring and dismiss effects fire.\n\tconst [panel, setPanelNode] = useElementRef<HTMLDivElement>();\n\n\tconst presence = usePresence(ctx.open);\n\n\t// The side and alignment the panel was ACTUALLY placed on — the requested\n\t// ones until `computePosition` flips or clamps them away from a viewport\n\t// edge. Seeded with the request rather than a hardcoded `\"bottom\"`, so the\n\t// common never-flipped case never depends on a first placement callback\n\t// having fired: a wrong seed would be a one-frame origin jump on every\n\t// open. The hook returns what the source kept in two `$state` locals fed\n\t// by `onPlacement`.\n\tconst { side: resolvedSide, align: resolvedAlign } = useAnchorPosition(panel, {\n\t\tanchor: () => ctx.triggerRef,\n\t\tside: ctx.side,\n\t\talign: ctx.align,\n\t\toffset: 4,\n\t});\n\n\t// `active: ctx.open` — a plain boolean where the source needed a getter.\n\t// The layer stays ON the stack for its whole exit and stops being TOP of\n\t// it the instant `open` flips, so a second Escape during the fade falls\n\t// through to whatever is underneath.\n\tuseDismissable(panel, {\n\t\tonDismiss: ctx.close,\n\t\texclude: () => [ctx.triggerRef],\n\t\tactive: ctx.open,\n\t});\n\n\t// Convention C-2: composed ABOVE any conditional. Calling this inside the\n\t// JSX branch below would be a conditional hook and would throw the first\n\t// time `mounted` flips.\n\tconst panelRef = useComposedRefs(\n\t\tsetPanelNode,\n\t\tforwardedRef,\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 they\n\t// were the instant the close began. React re-renders an exiting subtree\n\t// normally, so the freeze is explicit here, exactly as `ComboboxPanel` and\n\t// `AutocompletePanel` already do it. It is load-bearing on the most common\n\t// interaction there is: clicking a row commits the value and flips `open`\n\t// in the SAME turn, so an unfrozen panel would move the ✓ and the highlight\n\t// onto the clicked row and off the old one for the whole 150 ms fade.\n\tconst frozen = useRef({ options: ctx.options, value: ctx.value, activeIndex: ctx.activeIndex });\n\tif (ctx.open) {\n\t\tfrozen.current = { options: ctx.options, value: ctx.value, activeIndex: ctx.activeIndex };\n\t}\n\tconst view = frozen.current;\n\tconst selectedIndex = view.options.findIndex((o) => o.value === view.value);\n\n\tfunction rowClasses(index: number, option: SelectOption): string {\n\t\treturn cn(\n\t\t\t\"ft-select-option flex cursor-pointer items-center justify-between gap-2 rounded-[6px] px-[10px] py-[7px] text-foreground select-none\",\n\t\t\t(index === selectedIndex || index === view.activeIndex) &&\n\t\t\t\t\"bg-accent text-accent-foreground\",\n\t\t\toption.disabled && \"cursor-not-allowed opacity-50\"\n\t\t);\n\t}\n\n\tfunction handleClick(index: number, option: SelectOption): void {\n\t\tif (option.disabled) return;\n\t\tctx.commit(index);\n\t}\n\n\tfunction handlePointerEnter(index: number, option: SelectOption): void {\n\t\tif (option.disabled) return;\n\t\tctx.setActive(index);\n\t}\n\n\t// A real mousedown on ANY element carrying a `tabindex` attribute — even\n\t// `-1` — moves DOM focus to it as the browser's own default action,\n\t// independent of whether application JS ever calls `.focus()` on it.\n\t// Never calling `.focus()` ourselves is not what keeps focus on the\n\t// trigger; cancelling that default action is. Without this, clicking a\n\t// row focuses the row first (mousedown), the click then commits and\n\t// closes the panel, the row is removed from the DOM, and focus falls\n\t// through to `document.body`.\n\tfunction preventFocusSteal(event: ReactMouseEvent): void {\n\t\tevent.preventDefault();\n\t}\n\n\t// The `Portal` stays ABOVE the mounted gate and the gate wraps its\n\t// CHILDREN. `usePortalTarget` resolves its container in a layout effect,\n\t// so a `Portal` 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` looks for legs to start, and the entrance would be\n\t// silently skipped. Hoisting it resolves the container once, at this\n\t// component's own mount, which is `Select`'s mount: every later open\n\t// attaches its node in the very commit presence is waiting for.\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\t// The popup carries the trigger's own name: a listbox with\n\t\t\t\t\t// no accessible name is announced as an unnamed \"listbox\"\n\t\t\t\t\t// the instant the combobox expands, and every\n\t\t\t\t\t// `aria-activedescendant` announcement after it is made\n\t\t\t\t\t// inside that unnamed list.\n\t\t\t\t\taria-label={ctx.label}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"ft-select-panel border-border bg-popover text-popover-foreground flex w-max min-w-[160px] flex-col gap-[1px] rounded-[10px] border p-[5px] text-[13px] shadow-lg outline-none\",\n\t\t\t\t\t\tclassName\n\t\t\t\t\t)}\n\t\t\t\t\tdata-state={presence.surfaceState}\n\t\t\t\t\tdata-side={resolvedSide}\n\t\t\t\t\t// The REQUESTED alignment, not the resolved one, exactly as\n\t\t\t\t\t// the source publishes it — while `data-side` beside it is the\n\t\t\t\t\t// resolved value. The asymmetry is inherited deliberately\n\t\t\t\t\t// (fidelity over improvement); it is invisible until a clamp\n\t\t\t\t\t// actually moves the alignment, which needs a real viewport\n\t\t\t\t\t// edge.\n\t\t\t\t\tdata-align={ctx.align}\n\t\t\t\t\t// `transform-origin` only — never `position`, `left` or\n\t\t\t\t\t// `top`, which `useAnchorPosition` writes imperatively on\n\t\t\t\t\t// this same element.\n\t\t\t\t\tstyle={{ transformOrigin: originFor(resolvedSide, resolvedAlign) }}\n\t\t\t\t>\n\t\t\t\t\t{view.options.map((option, index) => (\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tKeyboard activation for a row is not local: the trigger\n\t\t\t\t\t\t\tbutton owns every keydown (Enter/Space/arrow keys/typeahead)\n\t\t\t\t\t\t\tand drives selection through `aria-activedescendant`, per the\n\t\t\t\t\t\t\tARIA combobox-with-listbox-popup pattern. A row-level keydown\n\t\t\t\t\t\t\thandler would be dead code — focus never reaches a row to fire\n\t\t\t\t\t\t\tone — not a missing affordance.\n\t\t\t\t\t\t*/\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tkey={option.value}\n\t\t\t\t\t\t\tid={ctx.optionId(index)}\n\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\taria-selected={index === selectedIndex}\n\t\t\t\t\t\t\taria-disabled={option.disabled ? \"true\" : undefined}\n\t\t\t\t\t\t\tclassName={rowClasses(index, option)}\n\t\t\t\t\t\t\tonMouseDown={preventFocusSteal}\n\t\t\t\t\t\t\tonClick={() => handleClick(index, option)}\n\t\t\t\t\t\t\tonPointerEnter={() => handlePointerEnter(index, option)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"truncate\">{option.label}</span>\n\t\t\t\t\t\t\t{index === selectedIndex ? (\n\t\t\t\t\t\t\t\t<span aria-hidden=\"true\" className=\"text-[var(--ft-field-accent)]\">\n\t\t\t\t\t\t\t\t\t✓\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</div>\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\nSelectPanel.displayName = \"SelectPanel\";\n","type":"registry:ui","target":"@ui/fancy/components/select/SelectPanel.tsx"},{"path":"react/src/components/select/index.ts","content":"\"use client\";\n\nexport { Select } from \"./Select\";\nexport type { SelectProps } from \"./Select\";\nexport type { SelectOption } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/select/index.ts"},{"path":"react/src/components/select/select-panel.css","content":"/* The source's `SelectPanel.svelte` <style> block, verbatim. Already anchored\n   on the row's own class (`.ft-select-option`, the first token of each row's\n   `cn()` call), so no port-added anchor class is needed here. The custom\n   property is re-declared locally with its full fallback chain on purpose: a\n   component must never depend on an app-level token existing. */\n.ft-select-option {\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}\n","type":"registry:ui","target":"@ui/fancy/components/select/select-panel.css"},{"path":"react/src/components/select/select.css","content":"/* The source's `Select.svelte` <style> block, verbatim. Already anchored on\n   the component's own root class (`.ft-select-trigger`, the first token of the\n   trigger's `cn()` call), so no port-added anchor class is needed here. */\n.ft-select-trigger: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/select/select.css"},{"path":"react/src/components/select/types.ts","content":"\"use client\";\n\nimport type { Side, Align } from \"../../internals/anchor-position\";\nimport { createInternalContext } from \"../../internals/dom/context\";\n\n/**\n * One selectable entry in a `Select`'s option list.\n *\n * A plain data shape, not a child component: `Select`'s whole API is driven\n * off an `options` array (closer to a native `<select>`'s `<option>` list\n * than to `RadioGroup`'s caller-composed items), so there is no\n * `SelectOption` component to import alongside it.\n */\nexport interface SelectOption {\n\t/** The value this option represents — what `Select`'s own `value` becomes when it is picked. */\n\tvalue: string;\n\t/** Visible text for this option, both in the trigger once selected and as a row in the panel. */\n\tlabel: string;\n\t/** Skips this option in keyboard navigation, typeahead, and pointer selection. */\n\tdisabled?: boolean;\n}\n\n/**\n * The contract between the `Select` trigger and its portalled listbox panel.\n *\n * `Select` owns the value, the option list, and where the active (highlighted)\n * index sits — everything the panel only ever reads or reports back through\n * `setActive`/`commit`. The panel never assigns `value` itself; `commit`\n * routes back through the root so `onValueChange` and the closing of the\n * panel stay in the one place that already handles both for every other way\n * a selection can be made (Enter, Tab, closed-state typeahead).\n */\nexport interface SelectContext {\n\t/** Whether the panel is open. Read by the panel's own presence clock, which\n\t *  needs it to tell an entrance from a departure, and by the panel's\n\t *  dismissable `active` gate: the instant this flips false the layer stops\n\t *  answering 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 trigger's `aria-controls` points at while open. */\n\treadonly panelId: string;\n\t/** The options to render as rows, in order. */\n\treadonly options: SelectOption[];\n\t/** The current value, `\"\"` when nothing is selected. */\n\treadonly value: string;\n\t/** The trigger's own accessible name (the `label` prop), so the portalled\n\t *  panel can carry the same name as `aria-label` — without this, a screen\n\t *  reader announces the listbox as unnamed the instant it expands, even\n\t *  though the combobox itself is named. `undefined` renders no attribute at\n\t *  all, matching the trigger's own `aria-label={label}`. */\n\treadonly label: string | undefined;\n\t/** Index of the highlighted option, or -1 when nothing is active. */\n\treadonly activeIndex: number;\n\t/** Side of the trigger to place the panel on. */\n\treadonly side: Side;\n\t/** Alignment along the trigger's cross axis. */\n\treadonly align: Align;\n\t/** The real trigger button — what the panel anchors against and excludes from its own outside-click check. */\n\treadonly triggerRef: HTMLElement | null;\n\t/** The id a given option row must carry — also what `aria-activedescendant` points at while it's active. */\n\toptionId(index: number): string;\n\t/** Whether the option at `index` is the current selection. */\n\tisSelected(index: number): boolean;\n\t/** Whether the option at `index` is the highlighted (`aria-activedescendant`) one. */\n\tisActive(index: number): boolean;\n\t/** Highlights `index` without selecting it — a pointer hover, not a commit. */\n\tsetActive(index: number): void;\n\t/** Selects the option at `index` and closes the panel, the same way Enter does. */\n\tcommit(index: number): void;\n\t/** Closes the panel without changing the value — what Escape and an outside click do. */\n\tclose(): void;\n}\n\n/**\n * The React counterpart of the source's `SELECT_KEY` symbol: a context object\n * plus its two readers, per the internals contract's naming table\n * (`FOO_KEY` → `FooReactContext` + `useFoo()`).\n *\n * Not re-exported from this folder's `index.ts`, because the source barrel\n * does not export `SELECT_KEY` either — the trigger/panel wiring is private to\n * the pair.\n */\nexport const SelectReactContext = createInternalContext<SelectContext>(\"Select context\");\n\n/** Reads the surrounding `Select`'s context. Throws outside one, which is the\n *  compound-component contract: `SelectPanel` is only ever rendered by\n *  `Select`, so there is no standalone-usage fallback to design for — the same\n *  as the source's `getContext(SELECT_KEY)` with no fallback branch. */\nexport const useSelectContext = SelectReactContext.useRequired;\n","type":"registry:ui","target":"@ui/fancy/components/select/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/select?framework=react"}}