{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toggle-group","type":"registry:ui","title":"ToggleGroup","description":"A segmented row of toggle buttons with roving-tabindex keyboard navigation and single or multiple selection.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["actions"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/toggle-group/ToggleGroup.tsx","content":"\"use client\";\n\nimport { forwardRef, useCallback, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { TOGGLE_GROUP_KEY } from \"./types\";\nimport type { ToggleGroupContext } from \"./types\";\nimport \"./toggle-group.css\";\n\nexport interface ToggleGroupProps {\n\t/** Whether one item can be active at a time, or several. Defaults to `\"single\"`. */\n\ttype?: \"single\" | \"multiple\";\n\t/**\n\t * The active value(s) — a string when `type=\"single\"`, an array of strings\n\t * when `type=\"multiple\"`. Omit it entirely to let the group own its own\n\t * selection and report every change through `onValueChange`.\n\t */\n\tvalue?: string | string[];\n\t/** Called with the new value, shaped to match `type`, whenever the selection changes. */\n\tonValueChange?: (value: string | string[]) => void;\n\t/** Disables every item in the group. */\n\tdisabled?: boolean;\n\t/** Sizes every item. */\n\tsize?: \"sm\" | \"md\" | \"lg\";\n\t/** The rail's stacking axis. Arrow keys work in both pairs regardless — see the README. */\n\torientation?: \"horizontal\" | \"vertical\";\n\t/** Accessible name for the group. */\n\tlabel?: string;\n\t/** The `ToggleGroupItem`s. */\n\tchildren?: ReactNode;\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\t/**\n\t * Form field name. When set, one hidden input named `name` is rendered per\n\t * selected value, so a single group submits its value and a multiple group\n\t * submits every selected one. Items are `type=\"button\"` and never submit.\n\t * A disabled group submits nothing. The selection is the group's data, so a\n\t * selected item that is disabled on its own still submits: disabling an item\n\t * locks that choice, it does not retract it.\n\t */\n\tname?: string;\n\t/** `id` of the `<form>` the hidden inputs belong to, when not nested in it. */\n\tform?: string;\n}\n\n/**\n * A rail of mutually-aware toggles with one selection model and one roving\n * tab stop.\n *\n * The root 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 not spread: the Svelte source reads only these props off\n * `$props()` and has no `...restProps`, so the port carries no wider\n * attribute surface than the component it mirrors.\n */\nexport const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(\n\t(\n\t\t{\n\t\t\ttype = \"single\",\n\t\t\tvalue: valueProp,\n\t\t\tonValueChange,\n\t\t\tdisabled = false,\n\t\t\tsize = \"md\",\n\t\t\torientation = \"horizontal\",\n\t\t\tlabel,\n\t\t\tchildren,\n\t\t\tclassName,\n\t\t\tsound = false,\n\t\t\tname,\n\t\t\tform,\n\t\t},\n\t\tforwardedRef\n\t) => {\n\t\t// The root queries its own subtree in `orderedEnabledButtons`, and the\n\t\t// consumer may also want the node. A plain ref rather than\n\t\t// `useElementRef`: nothing here is a hook keyed on the node's\n\t\t// existence (convention C-1's hazard) — the node is only ever read\n\t\t// from inside an event handler, by which time it is long since\n\t\t// attached — and a ref keeps every callback below identity-stable.\n\t\tconst rootRef = useRef<HTMLDivElement | null>(null);\n\t\tconst setRootRef = useComposedRefs(forwardedRef, rootRef);\n\n\t\t// The Svelte source's `value` is `$bindable(\"\")`: a consumer can bind\n\t\t// it, or leave it alone and let the component keep writing its own\n\t\t// copy. React has no such channel, so the prop is controlled when it\n\t\t// is passed and this local copy takes over when it is not. Either way\n\t\t// `onValueChange` fires with the same shape.\n\t\tconst [uncontrolledValue, setUncontrolledValue] = useState<string | string[]>(\"\");\n\t\tconst isControlled = valueProp !== undefined;\n\t\tconst value = isControlled ? valueProp : uncontrolledValue;\n\n\t\t// The public prop is a string or an array depending on `type`; everything\n\t\t// below works off one normalised shape so `isSelected`/`toggle` never\n\t\t// have to branch on which one they were handed.\n\t\tfunction toArray(current: string | string[]): string[] {\n\t\t\tif (type === \"single\") {\n\t\t\t\t// A single-select group has at most one active value, whatever\n\t\t\t\t// shape arrives — a caller handing it an array anyway (leftover\n\t\t\t\t// state from a `type` prop that used to be `\"multiple\"`, say)\n\t\t\t\t// gets just the first entry rather than every item lighting up\n\t\t\t\t// as selected at once.\n\t\t\t\tif (Array.isArray(current)) return current.length > 0 ? [current[0]!] : [];\n\t\t\t\treturn current === \"\" ? [] : [current];\n\t\t\t}\n\t\t\treturn Array.isArray(current) ? current : current === \"\" ? [] : [current];\n\t\t}\n\n\t\tconst selected = toArray(value);\n\n\t\t// Values currently taking part in roving focus. A disabled item never\n\t\t// appears here — see ToggleGroupItem's registration effect — so this\n\t\t// list doubles as \"mounted and enabled, in the order each one arrived\".\n\t\tconst [registeredOrder, setRegisteredOrder] = useState<string[]>([]);\n\n\t\tconst [focusedValueState, setFocusedValueState] = useState<string | null>(null);\n\n\t\tconst playCue = useSoundCue(sound);\n\n\t\tfunction isSelected(itemValue: string): boolean {\n\t\t\treturn selected.includes(itemValue);\n\t\t}\n\n\t\tfunction commit(next: string[]) {\n\t\t\tif (type === \"single\") {\n\t\t\t\tconst shaped = next[0] ?? \"\";\n\t\t\t\tif (!isControlled) setUncontrolledValue(shaped);\n\t\t\t\tonValueChange?.(shaped);\n\t\t\t} else {\n\t\t\t\tif (!isControlled) setUncontrolledValue(next);\n\t\t\t\tonValueChange?.(next);\n\t\t\t}\n\t\t}\n\n\t\tfunction toggle(itemValue: string) {\n\t\t\tif (disabled) return;\n\t\t\tconst current = toArray(value);\n\t\t\tconst isOn = current.includes(itemValue);\n\t\t\t// Three cues, chosen from the PRE-commit state and played before it:\n\t\t\t// a multiple-select rail reports the direction it is moving in, a\n\t\t\t// single-select one reports a pick — including the clear-on-repick\n\t\t\t// case below, which is still a pick from the reader's point of view.\n\t\t\t// The cue lives here rather than in the item's click handler, so an\n\t\t\t// item outside a group stays silent.\n\t\t\tplayCue(type === \"multiple\" ? (isOn ? \"toggle-off\" : \"toggle-on\") : \"select\");\n\t\t\tif (type === \"single\") {\n\t\t\t\t// Activating the already-active item clears the selection instead of\n\t\t\t\t// no-op-ing — the one state a native radio group can't express, and\n\t\t\t\t// the brief this component follows asks for it explicitly.\n\t\t\t\tcommit(isOn ? [] : [itemValue]);\n\t\t\t} else {\n\t\t\t\tcommit(isOn ? current.filter((v) => v !== itemValue) : [...current, itemValue]);\n\t\t\t}\n\t\t}\n\n\t\t// Both functions are commands, invoked from a ToggleGroupItem's own\n\t\t// effect — first to register on mount, then again as that same\n\t\t// effect's cleanup, to unregister. They are the one part of the\n\t\t// context that MUST keep a stable identity across renders: the item\n\t\t// lists them in its effect's dependency array, so a fresh function per\n\t\t// root render would re-run that effect on every root render, and the\n\t\t// re-run's cleanup (unregister) plus body (register) would alternate\n\t\t// forever. The updater also returns the array unchanged when there is\n\t\t// nothing to do, so a repeated register never schedules a render.\n\t\tconst register = useCallback((itemValue: string) => {\n\t\t\tsetRegisteredOrder((order) => (order.includes(itemValue) ? order : [...order, itemValue]));\n\t\t}, []);\n\n\t\tconst unregister = useCallback((itemValue: string) => {\n\t\t\tsetRegisteredOrder((order) => {\n\t\t\t\tconst index = order.indexOf(itemValue);\n\t\t\t\tif (index === -1) return order;\n\t\t\t\tconst next = order.slice();\n\t\t\t\tnext.splice(index, 1);\n\t\t\t\treturn next;\n\t\t\t});\n\t\t}, []);\n\n\t\tconst focus = useCallback((itemValue: string) => {\n\t\t\tsetFocusedValueState(itemValue);\n\t\t}, []);\n\n\t\t/**\n\t\t * The item buttons in actual DOM order, filtered to the enabled ones.\n\t\t * Queried fresh on every call instead of cached, so a reordered or\n\t\t * newly-mounted item is correct on the very next arrow press even though\n\t\t * nothing about mounting or registration told this component the order\n\t\t * had changed.\n\t\t */\n\t\tconst orderedEnabledButtons = useCallback((): HTMLButtonElement[] => {\n\t\t\tconst root = rootRef.current;\n\t\t\tif (!root) return [];\n\t\t\treturn Array.from(\n\t\t\t\troot.querySelectorAll<HTMLButtonElement>(\"[data-ft-toggle-item]:not(:disabled)\")\n\t\t\t);\n\t\t}, []);\n\n\t\tconst goTo = useCallback((button: HTMLButtonElement) => {\n\t\t\tconst nextValue = button.dataset.value;\n\t\t\tif (nextValue === undefined) return;\n\t\t\tsetFocusedValueState(nextValue);\n\t\t\tbutton.focus();\n\t\t}, []);\n\n\t\tconst move = useCallback(\n\t\t\t(from: string, delta: number) => {\n\t\t\t\tconst buttons = orderedEnabledButtons();\n\t\t\t\tif (buttons.length === 0) return;\n\t\t\t\tconst fromIndex = buttons.findIndex((el) => el.dataset.value === from);\n\t\t\t\tconst base = fromIndex === -1 ? 0 : fromIndex;\n\t\t\t\tconst nextIndex = (((base + delta) % buttons.length) + buttons.length) % buttons.length;\n\t\t\t\tgoTo(buttons[nextIndex]!);\n\t\t\t},\n\t\t\t[goTo, orderedEnabledButtons]\n\t\t);\n\n\t\tconst moveToEdge = useCallback(\n\t\t\t(edge: \"first\" | \"last\") => {\n\t\t\t\tconst buttons = orderedEnabledButtons();\n\t\t\t\tif (buttons.length === 0) return;\n\t\t\t\tgoTo(edge === \"first\" ? buttons[0]! : buttons[buttons.length - 1]!);\n\t\t\t},\n\t\t\t[goTo, orderedEnabledButtons]\n\t\t);\n\n\t\t// Recomputed on every render, so it settles the instant an item\n\t\t// registers, unregisters, or flips disabled — including when the item\n\t\t// that currently holds the roving position is the one that disappears.\n\t\tlet focusedValue: string | null = null;\n\t\tif (focusedValueState !== null && registeredOrder.includes(focusedValueState)) {\n\t\t\tfocusedValue = focusedValueState;\n\t\t} else if (registeredOrder.length > 0) {\n\t\t\t// Prefer the selected item so Tab lands on the active choice;\n\t\t\t// otherwise the first item to have registered.\n\t\t\tfocusedValue = registeredOrder.find((v) => selected.includes(v)) ?? registeredOrder[0]!;\n\t\t}\n\n\t\t// Rebuilt on every render rather than memoised, and that rebuild is what\n\t\t// re-renders the items — the React counterpart of the Svelte context's\n\t\t// live getters. A memo would have to be keyed on `selected`, which is a\n\t\t// fresh array each render by construction (its shape depends on `type`),\n\t\t// so the memo would either never hit or need a hand-rolled\n\t\t// serialisation. Nothing under a toggle rail is expensive enough to buy\n\t\t// that back. The two members an item depends on by identity —\n\t\t// `register` and `unregister` — are stable regardless.\n\t\tconst context: ToggleGroupContext = {\n\t\t\ttype,\n\t\t\tvalue: selected,\n\t\t\tdisabled,\n\t\t\tsize,\n\t\t\torientation,\n\t\t\tisSelected,\n\t\t\ttoggle,\n\t\t\tregister,\n\t\t\tunregister,\n\t\t\tfocusedValue,\n\t\t\tfocus,\n\t\t\tmove,\n\t\t\tmoveToEdge,\n\t\t};\n\n\t\treturn (\n\t\t\t<TOGGLE_GROUP_KEY.Provider value={context}>\n\t\t\t\t<div\n\t\t\t\t\tref={setRootRef}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"ft-toggle-group border-border bg-background inline-flex w-fit border\",\n\t\t\t\t\t\torientation === \"vertical\" ? \"flex-col\" : \"flex-row\",\n\t\t\t\t\t\tclassName\n\t\t\t\t\t)}\n\t\t\t\t\tdata-orientation={orientation}\n\t\t\t\t\trole=\"group\"\n\t\t\t\t\taria-label={label}\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t\t{name\n\t\t\t\t\t\t? selected.map((picked) => (\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tkey={picked}\n\t\t\t\t\t\t\t\t\ttype=\"hidden\"\n\t\t\t\t\t\t\t\t\tname={name}\n\t\t\t\t\t\t\t\t\tvalue={picked}\n\t\t\t\t\t\t\t\t\tform={form}\n\t\t\t\t\t\t\t\t\tdisabled={disabled || undefined}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t: null}\n\t\t\t\t</div>\n\t\t\t</TOGGLE_GROUP_KEY.Provider>\n\t\t);\n\t}\n);\n\nToggleGroup.displayName = \"ToggleGroup\";\n","type":"registry:ui","target":"@ui/fancy/components/toggle-group/ToggleGroup.tsx"},{"path":"react/src/components/toggle-group/ToggleGroupItem.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext, useEffect, useRef } from \"react\";\nimport type {\n\tComponentPropsWithoutRef,\n\tFocusEvent,\n\tKeyboardEvent,\n\tMouseEvent,\n\tReactNode,\n} from \"react\";\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { TOGGLE_GROUP_KEY } from \"./types\";\nimport \"./toggle-group-item.css\";\n\ninterface ToggleGroupItemOwnProps {\n\t/** This item's value — what gets added to or removed from the group's selection. */\n\tvalue: string;\n\t/** Disables just this item, independent of the group's own `disabled`. */\n\tdisabled?: boolean;\n\t/** Accessible name, for icon-only content. Falls back to the rendered content. */\n\tlabel?: string;\n\t/** The item's content, typically a glyph or a short label. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Native `<button>` attributes and listeners are forwarded to the item; the managed\n * ARIA, state and structural attributes stay the component's.\n */\nexport interface ToggleGroupItemProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"button\">, keyof ToggleGroupItemOwnProps>,\n\t\tToggleGroupItemOwnProps {}\n\nconst SIZE_CLASSES: Record<\"sm\" | \"md\" | \"lg\", string> = {\n\tsm: \"h-[26px] min-w-[26px] rounded-[4px] px-2 text-xs\",\n\tmd: \"h-[30px] min-w-[32px] rounded-[6px] px-2.5 text-sm\",\n\tlg: \"h-[34px] min-w-[36px] rounded-[8px] px-3 text-base\",\n};\n\n/**\n * One toggle in a `ToggleGroup` rail.\n *\n * The element arrives through the ref channel rather than a `ref` prop, per\n * PORTING.md — the Svelte source declares `ref = $bindable(null)`.\n */\nexport const ToggleGroupItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(\n\t(\n\t\t{ value, disabled = false, label, children, className, onClick, onFocus, onKeyDown, ...rest },\n\t\tforwardedRef\n\t) => {\n\t\t// Undefined outside a ToggleGroup: the item then has no selection or\n\t\t// roving order to take part in, and renders as a plain, always-tabbable,\n\t\t// permanently-unselected button rather than throwing.\n\t\tconst group = useContext(TOGGLE_GROUP_KEY);\n\n\t\t// The click handler moves DOM focus to this very button, so the\n\t\t// component needs the node itself even when the consumer passes no ref.\n\t\tconst innerRef = useRef<HTMLButtonElement | null>(null);\n\t\tconst setRef = useComposedRefs(forwardedRef, innerRef);\n\n\t\tconst isDisabled = disabled || (group?.disabled ?? false);\n\t\tconst isSelected = group?.isSelected(value) ?? false;\n\t\tconst size = group?.size ?? \"md\";\n\t\t// `undefined` outside a group leaves the native default (a plain button is\n\t\t// already in the tab order on its own); inside one, exactly the item\n\t\t// holding the roving position gets 0 and every other gets -1.\n\t\tconst tabIndexAttr = group ? (group.focusedValue === value ? 0 : -1) : undefined;\n\n\t\tconst classes = cn(\n\t\t\t// No `transition-colors`: the colocated stylesheet puts a `transition`\n\t\t\t// shorthand on this same element, and its rules are unlayered while\n\t\t\t// Tailwind utilities live in `@layer utilities`, so the utility\n\t\t\t// would have been replaced without a trace. The colour channel is\n\t\t\t// re-declared by hand there instead.\n\t\t\t\"ft-toggle-group-item inline-flex shrink-0 cursor-pointer items-center justify-center font-medium\",\n\t\t\t\"focus-visible:outline-none\",\n\t\t\t\"disabled:pointer-events-none disabled:opacity-50\",\n\t\t\tSIZE_CLASSES[size],\n\t\t\tisSelected\n\t\t\t\t? \"bg-secondary text-secondary-foreground\"\n\t\t\t\t: \"text-muted-foreground hover:bg-accent hover:text-accent-foreground\",\n\t\t\tclassName\n\t\t);\n\n\t\t// Joins the roving-focus order whenever this item is enabled, and leaves\n\t\t// it in every other case: disabled from the start, or going disabled\n\t\t// mid-session. That keeps the group's fallback tabbable position from\n\t\t// ever landing on a button that cannot actually take focus. The effect's\n\t\t// cleanup — run on unmount, and again before each re-run when `value` or\n\t\t// `isDisabled` changes — unregisters the value the previous run added.\n\t\t//\n\t\t// The dependency list holds the two commands themselves, never the\n\t\t// context object: the root rebuilds that object on every render, so\n\t\t// depending on it would re-run this effect (unregister, then register)\n\t\t// on every root render. `register`/`unregister` are identity-stable by\n\t\t// contract for exactly this reason.\n\t\tconst register = group?.register;\n\t\tconst unregister = group?.unregister;\n\t\tuseEffect(() => {\n\t\t\tif (!register || !unregister || isDisabled) return;\n\t\t\t// Captured locally: `value` inside the returned cleanup would otherwise\n\t\t\t// read whatever the prop is *when the effect next re-runs*, not what it\n\t\t\t// was when this run registered.\n\t\t\tconst registeredValue = value;\n\t\t\tregister(registeredValue);\n\t\t\treturn () => unregister(registeredValue);\n\t\t}, [register, unregister, isDisabled, value]);\n\n\t\t// The native `disabled` attribute below is the real gate, exactly as in\n\t\t// Toggle — but a synthetic event fired directly at the element (as a\n\t\t// test does, and as some assistive tech does) can still reach these\n\t\t// handlers without going through the browser's own pre-click disabled\n\t\t// check, so each one repeats the guard rather than trusting the\n\t\t// attribute alone.\n\t\tfunction handleClick(event: MouseEvent<HTMLButtonElement>) {\n\t\t\tif (isDisabled) return;\n\t\t\tgroup?.toggle(value);\n\t\t\t// Deliberate, not incidental: a plain `<button>` is only guaranteed\n\t\t\t// to take focus on click in some browsers (macOS Safari notably does\n\t\t\t// not, by default). Without this, a mouse click there would select\n\t\t\t// the item but leave the roving tab stop — and DOM focus — wherever\n\t\t\t// it last was, so the very next Tab lands somewhere the reader did\n\t\t\t// not just interact with. `focus()` on an element already focused is\n\t\t\t// a no-op, so this costs nothing on browsers that would have moved\n\t\t\t// focus here anyway.\n\t\t\tgroup?.focus(value);\n\t\t\tinnerRef.current?.focus();\n\t\t\tonClick?.(event);\n\t\t}\n\n\t\tfunction handleFocus(event: FocusEvent<HTMLButtonElement>) {\n\t\t\tif (isDisabled) return;\n\t\t\tgroup?.focus(value);\n\t\t\tonFocus?.(event);\n\t\t}\n\n\t\tfunction handleKeydown(event: KeyboardEvent<HTMLButtonElement>) {\n\t\t\tif (isDisabled) return;\n\t\t\thandleNavigationKey(event);\n\t\t\tonKeyDown?.(event);\n\t\t}\n\n\t\tfunction handleNavigationKey(event: KeyboardEvent<HTMLButtonElement>) {\n\t\t\tif (!group) return;\n\t\t\tswitch (event.key) {\n\t\t\t\tcase \"ArrowRight\":\n\t\t\t\tcase \"ArrowDown\":\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tgroup.move(value, 1);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowLeft\":\n\t\t\t\tcase \"ArrowUp\":\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tgroup.move(value, -1);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"Home\":\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tgroup.moveToEdge(\"first\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"End\":\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tgroup.moveToEdge(\"last\");\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn (\n\t\t\t<button\n\t\t\t\t{...rest}\n\t\t\t\tref={setRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tdata-ft-toggle-item=\"\"\n\t\t\t\tdata-value={value}\n\t\t\t\tdata-size={size}\n\t\t\t\tclassName={classes}\n\t\t\t\tdisabled={isDisabled}\n\t\t\t\taria-pressed={isSelected}\n\t\t\t\taria-label={label ?? rest[\"aria-label\"]}\n\t\t\t\ttabIndex={tabIndexAttr ?? rest.tabIndex}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tonFocus={handleFocus}\n\t\t\t\tonKeyDown={handleKeydown}\n\t\t\t>\n\t\t\t\t{children !== undefined ? children : (label ?? value)}\n\t\t\t</button>\n\t\t);\n\t}\n);\n\nToggleGroupItem.displayName = \"ToggleGroupItem\";\n","type":"registry:ui","target":"@ui/fancy/components/toggle-group/ToggleGroupItem.tsx"},{"path":"react/src/components/toggle-group/index.ts","content":"\"use client\";\n\nexport { ToggleGroup } from \"./ToggleGroup\";\nexport type { ToggleGroupProps } from \"./ToggleGroup\";\nexport { ToggleGroupItem } from \"./ToggleGroupItem\";\nexport type { ToggleGroupItemProps } from \"./ToggleGroupItem\";\nexport { TOGGLE_GROUP_KEY } from \"./types\";\nexport type { ToggleGroupContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/toggle-group/index.ts"},{"path":"react/src/components/toggle-group/toggle-group-item.css","content":"/*\n * Same brand-accent shape as Toggle's own `--ft-toggle-accent`: no\n * semantic token owns this purple, so it is declared locally with a\n * light-dark() fallback, retintable from higher up the tree via\n * `--ft-accent`.\n *\n * Every rule is anchored on `.ft-toggle-group-item` — the Svelte source's own\n * root class for this element, already the first token of its class list — so\n * nothing here escapes into the rest of a consumer's page now that Svelte's\n * compiler scoping is gone.\n */\n.ft-toggle-group-item {\n\t--ft-toggle-group-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\t/* One local alias so the token pair is typed once rather than six times.\n\t   150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) = tokens.EASINGS.inout */\n\t--ft-toggle-group-motion: var(--ft-duration-fast, 150ms)\n\t\tvar(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));\n\t/* Replaces the `transition-colors` utility removed from the class string\n\t   in ToggleGroupItem.tsx. Colour is a state change, not motion, so it stays\n\t   outside the reduced-motion query. `text-decoration-color`, `fill` and\n\t   `stroke` never change on this control, so the three that do are the\n\t   faithful subset of what the utility covered. */\n\ttransition:\n\t\tcolor var(--ft-toggle-group-motion),\n\t\tbackground-color var(--ft-toggle-group-motion),\n\t\tborder-color var(--ft-toggle-group-motion);\n\t/* Kills the ~300ms tap delay without blocking scroll — the same rule,\n\t   for the same reason, as `.ft-pressable`. */\n\ttouch-action: manipulation;\n}\n\n/* The focus ring stays an untransitioned `box-shadow`: a ring that fades in\n   is a ring a keyboard user has to wait for. */\n.ft-toggle-group-item:focus-visible {\n\tbox-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-toggle-group-accent) 35%, transparent);\n}\n\n/* Universal fallback — press feedback must survive reduced motion, and a UA\n   that supports neither query still needs some pressed affordance. */\n.ft-toggle-group-item:active:not(:disabled) {\n\topacity: var(--ft-toggle-group-press-opacity, 0.85);\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-toggle-group-item {\n\t\t/* The individual `scale` property, not `transform: scale()`: this\n\t\t   rule is unlayered, so a `transform` here would beat any transform\n\t\t   utility a consumer passes through the public `className` prop — a\n\t\t   `rotate-45` would silently vanish, at rest AND under the press.\n\t\t   `scale` composes with the consumer's `transform` instead of\n\t\t   replacing it. */\n\t\tscale: 1;\n\t\ttransition:\n\t\t\tcolor var(--ft-toggle-group-motion),\n\t\t\tbackground-color var(--ft-toggle-group-motion),\n\t\t\tborder-color var(--ft-toggle-group-motion),\n\t\t\tscale var(--ft-toggle-group-motion);\n\t}\n\n\t.ft-toggle-group-item:active:not(:disabled) {\n\t\tscale: var(--ft-toggle-group-press-scale, 0.97);\n\t\t/* Full motion = scale only; reduced motion = opacity only. Never both. */\n\t\topacity: 1;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/toggle-group/toggle-group-item.css"},{"path":"react/src/components/toggle-group/toggle-group.css","content":"/*\n * Exact rail padding/gap/radius from the mockup — not expressible as a\n * single Tailwind utility. The fill is `bg-background`, not `bg-muted`:\n * this app's dark theme has `--muted` *lighter* than `--card` (0.15 vs\n * 0.1 lightness), so a muted fill on a card-nested rail reads as raised,\n * the opposite of the mockup's recessed strip. `--background` is the\n * darkest token in dark mode and ties `--card` in light mode, so it can\n * only ever read as recessed or flush with its surroundings — never\n * inverted — at the cost of no visible fill contrast on a bare page in\n * light mode, where the 1px border alone carries the shape.\n *\n * `.ft-toggle-group` is the Svelte source's own root class, already the\n * first token of the root class list, so the rule below is anchored on it\n * exactly as the compiler-scoped original was.\n */\n.ft-toggle-group {\n\tborder-radius: 0.5rem; /* 8px */\n\tpadding: 3px;\n\tgap: 2px;\n}\n","type":"registry:ui","target":"@ui/fancy/components/toggle-group/toggle-group.css"},{"path":"react/src/components/toggle-group/types.ts","content":"\"use client\";\n\n/**\n * The contract between the toggle group root and its items.\n *\n * `ToggleGroup` owns the selection and the roving-focus position; every item\n * reads and drives both through context instead of a prop and a callback\n * threaded down by hand for each of them. An item mounted outside a group\n * degrades to a plain, ungrouped button rather than throwing — see\n * `ToggleGroupItem`.\n *\n * The arrow-key sequence is never read off `register`/`unregister` order:\n * the root re-queries the live DOM at the moment a key is pressed, so items\n * that mount out of order, or that get reordered by a keyed list after\n * mounting, still step through in the order they actually appear on screen.\n * Registration order is used only as the pre-interaction fallback for which\n * item starts out tabbable.\n */\n\nimport { createContext } from \"react\";\n\n/** What the root publishes. Items read it; only the root writes it. */\nexport interface ToggleGroupContext {\n\t/** Whether one value can be active at a time, or several. */\n\treadonly type: \"single\" | \"multiple\";\n\t/** The active values, always as an array — `type=\"single\"` just keeps at most one. */\n\treadonly value: string[];\n\t/** Disables every item, regardless of its own `disabled` prop. */\n\treadonly disabled: boolean;\n\t/** Sizes every item. */\n\treadonly size: \"sm\" | \"md\" | \"lg\";\n\t/** The rail's stacking axis. Both arrow-key pairs work in either orientation — see `move`. */\n\treadonly orientation: \"horizontal\" | \"vertical\";\n\t/** Whether `itemValue` is part of the current selection. */\n\tisSelected(itemValue: string): boolean;\n\t/** Activates or deactivates `itemValue`, following `type`'s rule. */\n\ttoggle(itemValue: string): void;\n\t/**\n\t * Joins the roving-focus order. An item skips this entirely while disabled.\n\t *\n\t * Identity-stable for the life of the root: it is called from an item's own\n\t * effect and therefore appears in that effect's dependency list. This is\n\t * the React counterpart of the `untrack` the Svelte source wraps the body\n\t * in — there, an effect that reads the registry it just mutated alternates\n\t * between register and unregister until the scheduler gives up; here, a\n\t * context member whose identity changed on every root render would re-run\n\t * the same effect on every root render, unregistering and re-registering\n\t * forever. The state updater is also a no-op returning the *same* array\n\t * when nothing changes, so a duplicate register never schedules a render.\n\t */\n\tregister(itemValue: string): void;\n\t/** Leaves the roving-focus order. Identity-stable, for the same reason as `register`. */\n\tunregister(itemValue: string): void;\n\t/** The value that currently carries `tabindex=\"0\"`, or `null` before anything has registered. */\n\treadonly focusedValue: string | null;\n\t/** Marks `itemValue` as the roving-focus position, without moving DOM focus. */\n\tfocus(itemValue: string): void;\n\t/**\n\t * Moves the roving-focus position `delta` steps from `from` in DOM order,\n\t * skipping disabled items and wrapping at both ends, and moves DOM focus\n\t * along with it.\n\t */\n\tmove(from: string, delta: number): void;\n\t/** Moves the roving-focus position, and DOM focus, to the first or last enabled item in DOM order. */\n\tmoveToEdge(edge: \"first\" | \"last\"): void;\n}\n\n/**\n * The context an item reads to find its surrounding group. The Svelte source\n * publishes it under a `Symbol` context key; React's own context object plays\n * that role here, so the exported name is kept and the value is a\n * `React.Context` rather than a symbol:\n *\n * ```tsx\n * const group = useContext(TOGGLE_GROUP_KEY);\n * ```\n *\n * Read it as optional — an item rendered outside a `ToggleGroup` gets\n * `undefined` rather than throwing.\n */\nexport const TOGGLE_GROUP_KEY = createContext<ToggleGroupContext | undefined>(undefined);\nTOGGLE_GROUP_KEY.displayName = \"ToggleGroupContext\";\n","type":"registry:ui","target":"@ui/fancy/components/toggle-group/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/toggle-group?framework=react"}}