{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tabs","type":"registry:ui","title":"Tabs","description":"A tablist compound with roving-tabindex keyboard navigation, automatic or manual activation, and an accent-underline or segmented visual style.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["navigation"],"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/tabs/Tabs.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 { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { TABS_KEY } from \"./types\";\nimport type { TabsContext } from \"./types\";\nimport \"./tabs.css\";\n\nexport interface TabsProps {\n\t/**\n\t * The active tab's value. Passing it makes the selection controlled — the\n\t * React counterpart of the Svelte source's `$bindable`; omit it entirely to\n\t * let the root own its own selection and report every change through\n\t * `onValueChange`.\n\t */\n\tvalue?: string;\n\t/** Called with the new value whenever the active tab changes. */\n\tonValueChange?: (value: string) => void;\n\t/** The tablist's stacking axis and which arrow-key pair moves it. Defaults to `\"horizontal\"`. */\n\torientation?: \"horizontal\" | \"vertical\";\n\t/**\n\t * Whether arrowing to a trigger selects it immediately (`\"automatic\"`),\n\t * or only moves focus, leaving Enter/Space to select (`\"manual\"`).\n\t * Defaults to `\"automatic\"`.\n\t */\n\tactivation?: \"automatic\" | \"manual\";\n\t/** Accent underline, or a segmented pill rail. Defaults to `\"underline\"`. */\n\tvariant?: \"underline\" | \"segmented\";\n\t/** A `TabsList` and one or more `TabsContent`s. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n\t/**\n\t * Plays the select cue through the sound controller. Off by default;\n\t * only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n/**\n * The root of the tabs compound: it owns the active value, the roving-focus\n * position, and the id pair every trigger/panel couple is wired up with.\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 attribute\n * surface than the component it mirrors.\n */\nexport const Tabs = forwardRef<HTMLDivElement, TabsProps>(\n\t(\n\t\t{\n\t\t\tvalue: valueProp,\n\t\t\tonValueChange,\n\t\t\torientation = \"horizontal\",\n\t\t\tactivation = \"automatic\",\n\t\t\tvariant = \"underline\",\n\t\t\tchildren,\n\t\t\tclassName,\n\t\t\tsound = false,\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 existence\n\t\t// (convention C-1's hazard) — the node is only ever read from inside an\n\t\t// event handler or an effect, by which time it is long since attached —\n\t\t// 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// SSR-stable: `internals/use-id.js`'s `uid()` is client-only, and\n\t\t// trigger/panel ids must already agree on the very first server-rendered\n\t\t// paint so `aria-controls`/`aria-labelledby` are correct before hydration.\n\t\tconst uid = useFancyId();\n\n\t\tconst playCue = useSoundCue(sound);\n\n\t\t// The Svelte source's `value` is `$bindable(\"\")`: a consumer can bind it,\n\t\t// or leave it alone and let the component keep writing its own copy.\n\t\t// React has no such channel, so the prop is controlled when it is passed\n\t\t// and this local copy takes over when it is not. Either way\n\t\t// `onValueChange` fires with the same value.\n\t\tconst [uncontrolledValue, setUncontrolledValue] = useState(\"\");\n\t\tconst isControlled = valueProp !== undefined;\n\t\tconst value = isControlled ? valueProp : uncontrolledValue;\n\n\t\tfunction isSelected(itemValue: string): boolean {\n\t\t\treturn value === itemValue;\n\t\t}\n\n\t\t// The only place `value` changes.\n\t\t//\n\t\t// The cue is guarded on an actual move: re-activating the tab that is\n\t\t// already active is silent, while `onValueChange` fires either way. The\n\t\t// comparison has to happen before the write, exactly as it does in the\n\t\t// source, so the guard reads the outgoing value rather than the one it\n\t\t// is about to become.\n\t\tfunction select(itemValue: string) {\n\t\t\tconst changed = value !== itemValue;\n\t\t\tif (!isControlled) setUncontrolledValue(itemValue);\n\t\t\tif (changed) playCue(\"select\");\n\t\t\tonValueChange?.(itemValue);\n\t\t}\n\n\t\t// Values currently taking part in roving focus. A disabled trigger never\n\t\t// appears here — see TabsTrigger's registration effect — so this list\n\t\t// doubles as \"mounted and enabled, in the order each one arrived\". Used\n\t\t// only as the pre-interaction tabbable fallback; `move`/`moveToEdge`\n\t\t// below re-query the live DOM instead of trusting this order, exactly\n\t\t// like ToggleGroup.\n\t\tconst [registeredOrder, setRegisteredOrder] = useState<string[]>([]);\n\n\t\tconst [focusedValueState, setFocusedValueState] = useState<string | null>(null);\n\n\t\t// Both functions are commands, invoked from a TabsTrigger's own effect —\n\t\t// first to register on mount, then again as that same effect's cleanup,\n\t\t// to unregister. They are the one part of the context that MUST keep a\n\t\t// stable identity across renders: the trigger lists them in its effect's\n\t\t// dependency array, so a fresh function per root render would re-run that\n\t\t// effect on every root render, and the re-run's cleanup (unregister) plus\n\t\t// body (register) would alternate forever. See the note on\n\t\t// `TabsContext.register` — this codebase has already shipped that\n\t\t// infinite loop once, on ToggleGroup.\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 trigger 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 trigger is correct on the very next arrow press even\n\t\t * though nothing about mounting or registration told this component the\n\t\t * order 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-tabs-trigger]:not(:disabled)\")\n\t\t\t);\n\t\t}, []);\n\n\t\tfunction goTo(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\t// Fused focus+select: with automatic activation, arrowing onto a tab\n\t\t\t// is what activates it — the WAI-ARIA Tabs pattern's default. Manual\n\t\t\t// activation only moves focus here; the trigger's own click handler\n\t\t\t// (which a native button already fires for Enter/Space) selects.\n\t\t\tif (activation === \"automatic\") select(nextValue);\n\t\t}\n\n\t\tfunction move(from: string, delta: number) {\n\t\t\tconst buttons = orderedEnabledButtons();\n\t\t\tif (buttons.length === 0) return;\n\t\t\tconst fromIndex = buttons.findIndex((el) => el.dataset.value === from);\n\t\t\tconst base = fromIndex === -1 ? 0 : fromIndex;\n\t\t\tconst nextIndex = (((base + delta) % buttons.length) + buttons.length) % buttons.length;\n\t\t\tgoTo(buttons[nextIndex]!);\n\t\t}\n\n\t\tfunction moveToEdge(edge: \"first\" | \"last\") {\n\t\t\tconst buttons = orderedEnabledButtons();\n\t\t\tif (buttons.length === 0) return;\n\t\t\tgoTo(edge === \"first\" ? buttons[0]! : buttons[buttons.length - 1]!);\n\t\t}\n\n\t\t// Deliberately does not call `select` — unlike `goTo`, which arrow-key\n\t\t// navigation drives and which fuses focus with selection under automatic\n\t\t// activation. This is used to *reclaim* DOM focus after a trigger\n\t\t// disappears out from under it (going disabled), and disabling the\n\t\t// selected trigger must not change the selection or the visible panel —\n\t\t// only where DOM focus lands.\n\t\t//\n\t\t// Identity-stable: a trigger reads it from an effect's dependency list.\n\t\tconst focusElement = useCallback(\n\t\t\t(itemValue: string) => {\n\t\t\t\tconst button = orderedEnabledButtons().find((el) => el.dataset.value === itemValue);\n\t\t\t\tif (!button) return;\n\t\t\t\tsetFocusedValueState(itemValue);\n\t\t\t\tbutton.focus();\n\t\t\t},\n\t\t\t[orderedEnabledButtons]\n\t\t);\n\n\t\t// Recomputed on every render, so it settles the instant a trigger\n\t\t// registers, unregisters, or flips disabled — including when the trigger\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 tab so Tab lands on the active one; otherwise\n\t\t\t// the first trigger to have registered.\n\t\t\tfocusedValue = registeredOrder.find((v) => v === value) ?? 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 pieces below — the React counterpart of the Svelte\n\t\t// context's live getters. The three members a trigger depends on by\n\t\t// identity — `register`, `unregister` and `focusElement` — are stable\n\t\t// regardless.\n\t\tconst context: TabsContext = {\n\t\t\tvalue,\n\t\t\torientation,\n\t\t\tactivation,\n\t\t\tvariant,\n\t\t\tisSelected,\n\t\t\tselect,\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\tfocusElement,\n\t\t\ttriggerId(itemValue) {\n\t\t\t\treturn `${uid}-trigger-${itemValue}`;\n\t\t\t},\n\t\t\tpanelId(itemValue) {\n\t\t\t\treturn `${uid}-panel-${itemValue}`;\n\t\t\t},\n\t\t};\n\n\t\treturn (\n\t\t\t<TABS_KEY.Provider value={context}>\n\t\t\t\t<div ref={setRootRef} className={cn(\"ft-tabs\", className)} data-orientation={orientation}>\n\t\t\t\t\t{children}\n\t\t\t\t</div>\n\t\t\t</TABS_KEY.Provider>\n\t\t);\n\t}\n);\n\nTabs.displayName = \"Tabs\";\n","type":"registry:ui","target":"@ui/fancy/components/tabs/Tabs.tsx"},{"path":"react/src/components/tabs/TabsContent.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { runTransition } from \"../../internals/motion/animate\";\nimport type { TransitionRun } from \"../../internals/motion/animate\";\nimport { preset } from \"../../internals/motion/transitions\";\nimport { prefersReducedMotion } from \"../../internals/motion/anchored\";\nimport { DURATIONS } from \"../../internals/motion/tokens\";\nimport { TABS_KEY } from \"./types\";\n\nexport interface TabsContentProps {\n\t/** Which `TabsTrigger` shows this panel. */\n\tvalue: string;\n\t/**\n\t * Keeps this panel mounted in the DOM (with the `hidden` attribute)\n\t * even while inactive, instead of the default of unmounting it\n\t * entirely. Needed for content — an iframe, a video, a form with\n\t * uncommitted input — that must not remount every time the user tabs\n\t * away and back.\n\t */\n\tforceMount?: boolean;\n\t/** The panel's content. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n}\n\n// An ENTRANCE, never a cross-fade, and an enter-only leg rather than a\n// two-way transition.\n//\n// TabsContent instances are siblings the caller places by hand: there is no\n// shared container to stack an outgoing panel inside, and each panel owns its\n// own conditional below. A true cross-fade would need the outgoing panel taken\n// out of flow inside a containing block this component does not own and cannot\n// create without wrapping every caller's content in a layer element — a\n// permanent structural change to every panel in the library, for one 150ms\n// dissolve. So the panel being left cuts away exactly as it always did, and\n// only the arriving one is animated: the hard cut is the defect that was\n// actually visible.\n//\n// Because the entrance never delays an unmount, nothing that observes the swap\n// changes — the previous panel is out of the DOM in the same commit the new one\n// lands, and no assertion in the suite had to be rewritten for this.\n//\n// Module scope, not per instance: `preset()` is a pure factory returning a\n// pure function, so one instance serves every panel.\nconst panelFade = preset(\"fade\");\n\n/**\n * One tab's panel. Rendered only while its trigger is selected, unless\n * `forceMount` keeps it in the DOM behind the `hidden` attribute.\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)`. Rest props\n * are not spread, for the same reason as on the root.\n */\nexport const TabsContent = forwardRef<HTMLDivElement, TabsContentProps>(\n\t({ value, forceMount = false, children, className }, forwardedRef) => {\n\t\t// Undefined outside a Tabs root: nothing is ever \"selected\", so this\n\t\t// panel only renders when `forceMount` is set, matching the graceful\n\t\t// degradation every other compound piece in this library falls back to.\n\t\tconst context = useContext(TABS_KEY);\n\t\tconst isSelected = context?.isSelected(value) ?? false;\n\t\tconst rendered = isSelected || forceMount;\n\n\t\tconst innerRef = useRef<HTMLDivElement | null>(null);\n\t\tconst setRef = useComposedRefs(forwardedRef, innerRef);\n\n\t\t// The entrance plays on a real selection change only, not on first\n\t\t// render: Svelte's local `in:` runs only once the block that owns it has\n\t\t// already run, so a panel that starts selected simply appears. The React\n\t\t// counterpart is the false→true EDGE of `rendered`, seeded from the very\n\t\t// first render — never a one-shot \"have I run yet\" latch, which\n\t\t// StrictMode's mount → cleanup → mount rehearsal would consume on the\n\t\t// first invocation and leave the second one animating a panel that\n\t\t// started selected. Tracking the edge instead makes every repeated setup\n\t\t// on the same `rendered` value a no-op, which is what a dev-only\n\t\t// rehearsal has to be. With `forceMount` every panel is mounted\n\t\t// permanently and the entrance never plays at all after that first\n\t\t// render — correct, since `forceMount` exists precisely to keep panels\n\t\t// alive and there is no way to animate a `hidden` attribute flip.\n\t\tconst prevRenderedRef = useRef(rendered);\n\n\t\t// A layout effect, per the effect-phase policy: Svelte starts intros\n\t\t// pre-paint, and a passive effect would paint one frame at rest first.\n\t\t//\n\t\t// `prefersReducedMotion()` is called here, at the instant the leg starts,\n\t\t// rather than kept as state: the preference is read then, never at\n\t\t// construction and never during SSR. `duration: 0` makes `runTransition`\n\t\t// finish synchronously and never touch `element.animate()`.\n\t\tuseIsomorphicLayoutEffect(() => {\n\t\t\tconst wasRendered = prevRenderedRef.current;\n\t\t\tprevRenderedRef.current = rendered;\n\t\t\tif (wasRendered || !rendered) return;\n\t\t\tconst el = innerRef.current;\n\t\t\tif (!el) return;\n\t\t\tlet run: TransitionRun | undefined;\n\t\t\trun = runTransition(\n\t\t\t\tel,\n\t\t\t\tpanelFade(el, { duration: prefersReducedMotion() ? 0 : DURATIONS.fast }, {\n\t\t\t\t\tdirection: \"in\",\n\t\t\t\t}),\n\t\t\t\t1,\n\t\t\t\tundefined,\n\t\t\t\t() => {\n\t\t\t\t\t// On enter finish, abort: that drops the `fill: forwards` so\n\t\t\t\t\t// the panel falls back to its resting style instead of\n\t\t\t\t\t// carrying a finished animation — whose output outranks\n\t\t\t\t\t// author CSS — for the rest of its life.\n\t\t\t\t\trun?.abort();\n\t\t\t\t\trun = undefined;\n\t\t\t\t}\n\t\t\t);\n\t\t\t// And a leg still in flight when the panel leaves is cancelled\n\t\t\t// rather than left running against a detached node.\n\t\t\treturn () => {\n\t\t\t\trun?.abort();\n\t\t\t\trun = undefined;\n\t\t\t};\n\t\t}, [rendered]);\n\n\t\tif (!rendered) return null;\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={setRef}\n\t\t\t\tid={context?.panelId(value)}\n\t\t\t\trole=\"tabpanel\"\n\t\t\t\taria-labelledby={context?.triggerId(value)}\n\t\t\t\ttabIndex={0}\n\t\t\t\thidden={!isSelected}\n\t\t\t\tclassName={cn(\"ft-tabs-content\", className)}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t);\n\t}\n);\n\nTabsContent.displayName = \"TabsContent\";\n","type":"registry:ui","target":"@ui/fancy/components/tabs/TabsContent.tsx"},{"path":"react/src/components/tabs/TabsList.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { prefersReducedMotion } from \"../../internals/motion/anchored\";\nimport { DURATIONS } from \"../../internals/motion/tokens\";\nimport { TABS_KEY } from \"./types\";\nimport \"./tabs-list.css\";\n\nexport interface TabsListProps {\n\t/** The `TabsTrigger`s. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes. */\n\tclassName?: string;\n}\n\n/** Backstop that drops `will-change` again after a tween.\n *\n * `will-change: transform` is a promise to the compositor, not a style;\n * leaving it on permanently costs a permanent layer for a bar that moves a few\n * times a session. `transitionend` is the honest signal to drop it, but jsdom\n * never fires one, so the release is keyed to the same token the CSS\n * transition below uses plus a slack margin — deterministic in both. */\nconst WILL_CHANGE_RELEASE = DURATIONS.fast + 50;\n\n/**\n * The rail of triggers, plus the single sliding indicator that paints the\n * selection.\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)`. Rest props\n * are not spread, for the same reason as on the root.\n */\nexport const TabsList = forwardRef<HTMLDivElement, TabsListProps>(\n\t({ children, className }, forwardedRef) => {\n\t\t// Undefined outside a Tabs root: orientation/variant then fall back to\n\t\t// this component's own defaults rather than throwing, matching every\n\t\t// other compound in this library.\n\t\tconst context = useContext(TABS_KEY);\n\t\tconst orientation = context?.orientation ?? \"horizontal\";\n\t\tconst variant = context?.variant ?? \"underline\";\n\n\t\tconst classes = cn(\n\t\t\t\"ft-tabs-list inline-flex\",\n\t\t\torientation === \"vertical\" ? \"flex-col\" : \"flex-row\",\n\t\t\tvariant === \"underline\"\n\t\t\t\t? cn(\"border-border\", orientation === \"vertical\" ? \"border-r\" : \"border-b\")\n\t\t\t\t: \"ft-tabs-list-segmented bg-background border-border w-fit border\",\n\t\t\tclassName\n\t\t);\n\n\t\t/*\n\t\t * ---------------------------------------------------------------------\n\t\t * The sliding indicator\n\t\t * ---------------------------------------------------------------------\n\t\t *\n\t\t * A single `aria-hidden` box, absolutely positioned at the list's own\n\t\t * origin and moved with `transform` alone — never `left`/`width`, which\n\t\t * would relayout the whole rail on every keystroke of an arrow-key walk.\n\t\t * It is a 1×1 box that `translate()` puts at the selected trigger and\n\t\t * `scale()` stretches to its size, so one transform carries both the\n\t\t * position and the length.\n\t\t *\n\t\t * It is a progressive enhancement, never the only selection signal: the\n\t\t * selected trigger keeps `aria-selected=\"true\"` and its own foreground\n\t\t * colour, so a screen reader, a forced-colors user and a JS-off render\n\t\t * all still see which tab is active.\n\t\t */\n\n\t\t// Plain refs, not `useElementRef`: both nodes are rendered\n\t\t// unconditionally, so they are attached by the time any effect below\n\t\t// runs — convention C-1's hazard is a node whose very existence is\n\t\t// conditional, which neither of these is.\n\t\tconst listRef = useRef<HTMLDivElement | null>(null);\n\t\tconst setListRef = useComposedRefs(forwardedRef, listRef);\n\t\tconst indicatorRef = useRef<HTMLDivElement | null>(null);\n\n\t\tconst willChangeTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n\t\t// Snapshots of the inputs the last placement ran with. Refs, not state —\n\t\t// only the placement effect reads or writes them, and nothing needs to\n\t\t// re-render when they change. They are what lets one effect tell three\n\t\t// cases apart: a selection moving (tween), a geometry change (snap,\n\t\t// because tweening between two different geometries is meaningless), and\n\t\t// the very first paint (snap, because the bar must not fly in from\n\t\t// `translate(0) scale(0)`).\n\t\tconst placedOnceRef = useRef(false);\n\t\tconst lastValueRef = useRef<string | undefined>(undefined);\n\t\tconst lastOrientationRef = useRef<\"horizontal\" | \"vertical\" | undefined>(undefined);\n\t\tconst lastVariantRef = useRef<\"underline\" | \"segmented\" | undefined>(undefined);\n\n\t\tfunction activeTrigger(): HTMLElement | null {\n\t\t\treturn (\n\t\t\t\tlistRef.current?.querySelector<HTMLElement>(\n\t\t\t\t\t'[data-ft-tabs-trigger][aria-selected=\"true\"]'\n\t\t\t\t) ?? null\n\t\t\t);\n\t\t}\n\n\t\tfunction releaseWillChange(): void {\n\t\t\tclearTimeout(willChangeTimerRef.current);\n\t\t\twillChangeTimerRef.current = undefined;\n\t\t\tconst el = indicatorRef.current;\n\t\t\tif (el) el.style.willChange = \"\";\n\t\t}\n\n\t\t/**\n\t\t * Writes the indicator's transform. `animate: false` snaps: first paint,\n\t\t * a resize, an orientation/variant change, and every write made while the\n\t\t * user asks for reduced motion — the bar still tracks the selection then,\n\t\t * it just arrives without a tween.\n\t\t *\n\t\t * Wrapped in `useEventCallback` so the long-lived observer callbacks\n\t\t * below always call the latest render's version — the React counterpart\n\t\t * of a Svelte function closing over live reactive state.\n\t\t */\n\t\tconst place = useEventCallback((animate: boolean): void => {\n\t\t\tconst el = indicatorRef.current;\n\t\t\tif (!el) return;\n\n\t\t\tconst active = activeTrigger();\n\t\t\tif (!active) {\n\t\t\t\t// Nothing is selected — `value` was never set, or points at a\n\t\t\t\t// value no trigger claims. The bar is not \"at zero\", it is absent.\n\t\t\t\tel.style.opacity = \"0\";\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst vertical = orientation === \"vertical\";\n\t\t\tconst off = vertical ? active.offsetTop : active.offsetLeft;\n\t\t\tconst size = vertical ? active.offsetHeight : active.offsetWidth;\n\n\t\t\t// A zero-size measurement is jsdom, a `display: none` ancestor, or a\n\t\t\t// first paint the browser has not laid out yet — never a real state.\n\t\t\t// Stay hidden rather than collapsing to a 0-wide sliver at the origin.\n\t\t\tif (size === 0) {\n\t\t\t\tel.style.opacity = \"0\";\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tel.style.opacity = \"1\";\n\n\t\t\t// The segmented pill is the trigger's whole box, so it stretches on\n\t\t\t// both axes; the underline bar is a 2px rule that only ever grows\n\t\t\t// along the rail's own axis.\n\t\t\tconst transform =\n\t\t\t\tvariant === \"segmented\"\n\t\t\t\t\t? `translate(${active.offsetLeft}px, ${active.offsetTop}px) scale(${active.offsetWidth}, ${active.offsetHeight})`\n\t\t\t\t\t: vertical\n\t\t\t\t\t\t? `translateY(${off}px) scaleY(${size})`\n\t\t\t\t\t\t: `translateX(${off}px) scaleX(${size})`;\n\n\t\t\tif (variant === \"segmented\") {\n\t\t\t\t// Feeds the `calc()` in the CSS that pre-divides the pill's radius\n\t\t\t\t// by its own scale factors, so the rendered corner keeps the\n\t\t\t\t// theme's own radius instead of being stretched into an ellipse.\n\t\t\t\t// Written by JS, and therefore internal — not a knob a consumer\n\t\t\t\t// should set.\n\t\t\t\tel.style.setProperty(\"--ft-tabs-indicator-sx\", String(active.offsetWidth));\n\t\t\t\tel.style.setProperty(\"--ft-tabs-indicator-sy\", String(active.offsetHeight));\n\t\t\t}\n\n\t\t\t// The selection did not move — this is a focus walk (the effect below\n\t\t\t// reads `focusedValue`), or a resize that changed nothing. Rewriting a\n\t\t\t// byte-identical transform would force a layout for nothing, and — on\n\t\t\t// the `animate: false` path, which suspends the transition — would\n\t\t\t// cancel a slide that is still in flight and teleport the bar to its\n\t\t\t// destination.\n\t\t\tif (el.style.transform === transform) return;\n\n\t\t\t// Nothing to tween FROM: the bar has never been successfully placed\n\t\t\t// (nothing was selected at mount, or the last measurement was zero, so\n\t\t\t// the branches above returned before writing anything). A transition\n\t\t\t// from the identity matrix would fly a 1x1 box in from the list's own\n\t\t\t// origin — the same artifact the first-paint snap exists to prevent.\n\t\t\tif (animate && !el.style.transform) animate = false;\n\n\t\t\tif (animate) {\n\t\t\t\tel.style.willChange = \"transform\";\n\t\t\t\tel.style.transform = transform;\n\t\t\t\tclearTimeout(willChangeTimerRef.current);\n\t\t\t\twillChangeTimerRef.current = setTimeout(releaseWillChange, WILL_CHANGE_RELEASE);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Suspend → write → force a reflow → restore. Without the forced read\n\t\t\t// between suspending and restoring, the browser coalesces all three\n\t\t\t// writes into one style recalculation and the \"snap\" tweens anyway.\n\t\t\t// Restoring the *previous* value rather than clearing it is what keeps\n\t\t\t// a consumer's own inline `transition` (or a later tween) alive.\n\t\t\treleaseWillChange();\n\t\t\tconst previous = el.style.transition;\n\t\t\tel.style.transition = \"none\";\n\t\t\tel.style.transform = transform;\n\t\t\tvoid el.offsetWidth;\n\t\t\tel.style.transition = previous;\n\t\t});\n\n\t\t// Everything that outlives a single render lives here, and this effect\n\t\t// depends on nothing that changes — deliberately NOT on the selection.\n\t\t//\n\t\t// DEVIATION from the contract's \"one effect owns all of it\": a\n\t\t// `ResizeObserver` delivers a callback as soon as `observe()` runs, so\n\t\t// folding the observer into the placement effect below would tear it down\n\t\t// and re-establish it on every tab change, and that first callback would\n\t\t// land right after the tween's write and snap it. Keeping it in its own\n\t\t// mount-scoped effect keeps the observer alive across selections; the\n\t\t// cleanup still does both jobs the contract asks of it, on unmount.\n\t\tuseIsomorphicLayoutEffect(() => {\n\t\t\tconst list = listRef.current;\n\t\t\tif (!list) return;\n\n\t\t\t// The list's own box changing means every trigger's offset may have\n\t\t\t// moved: a wrap, a font swap, a sibling arriving. R8: a resize snaps,\n\t\t\t// it never tweens — the bar was never \"travelling\" to its new place.\n\t\t\tconst observer = new ResizeObserver(() => place(false));\n\t\t\tobserver.observe(list);\n\n\t\t\t// The list's border box is not enough on its own. Inside a fixed-width\n\t\t\t// `TabsList` a trigger can change its own label — and therefore its\n\t\t\t// width and every later trigger's offset — while the list measures the\n\t\t\t// same to the pixel. Observing the triggers as well is what catches it.\n\t\t\tfunction observeTriggers(el: HTMLElement): void {\n\t\t\t\tfor (const trigger of el.querySelectorAll<HTMLElement>(\"[data-ft-tabs-trigger]\")) {\n\t\t\t\t\t// `ResizeObserver.observe` on an element already observed is a\n\t\t\t\t\t// no-op, so re-running this on every mutation costs nothing and\n\t\t\t\t\t// saves tracking which triggers are new.\n\t\t\t\t\tobserver.observe(trigger);\n\t\t\t\t}\n\t\t\t}\n\t\t\tobserveTriggers(list);\n\n\t\t\t// And geometry can move with NO box changing size at all: reordering\n\t\t\t// equal-width keyed tabs swaps two offsets while every observed border\n\t\t\t// box stays identical, so no resize ever fires. The mutation is the\n\t\t\t// only signal there is — it also picks up triggers that arrive later,\n\t\t\t// which is why `observeTriggers` runs again from here.\n\t\t\tconst mutations = new MutationObserver(() => {\n\t\t\t\tobserveTriggers(list);\n\t\t\t\tplace(false);\n\t\t\t});\n\t\t\tmutations.observe(list, { childList: true, subtree: true, characterData: true });\n\n\t\t\treturn () => {\n\t\t\t\tobserver.disconnect();\n\t\t\t\tmutations.disconnect();\n\t\t\t\treleaseWillChange();\n\t\t\t};\n\t\t\t// `place` is identity-stable for the life of the component; nothing\n\t\t\t// else here is read from a render scope.\n\t\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\t}, [place]);\n\n\t\tconst value = context?.value ?? \"\";\n\t\t// Read, not used: `focusedValue` recomputes from the roving-focus\n\t\t// registry, so a trigger mounting or unmounting re-runs this effect and\n\t\t// re-places the bar. The list's own `ResizeObserver` catches the same\n\t\t// thing in a browser, but only once layout has settled — this gets there\n\t\t// in the same commit.\n\t\tconst focusedValue = context?.focusedValue ?? null;\n\n\t\tuseIsomorphicLayoutEffect(() => {\n\t\t\tconst first = !placedOnceRef.current;\n\t\t\tconst geometryChanged =\n\t\t\t\torientation !== lastOrientationRef.current || variant !== lastVariantRef.current;\n\t\t\tconst selectionMoved = value !== lastValueRef.current;\n\n\t\t\tplacedOnceRef.current = true;\n\t\t\tlastValueRef.current = value;\n\t\t\tlastOrientationRef.current = orientation;\n\t\t\tlastVariantRef.current = variant;\n\n\t\t\t// The tween is the one case where all three conditions hold. Reduced\n\t\t\t// motion is read here, at the instant of the write, rather than kept\n\t\t\t// as reactive state: a preference that flips mid-flight should not\n\t\t\t// retro-cancel a bar already on its way.\n\t\t\tplace(selectionMoved && !first && !geometryChanged && !prefersReducedMotion());\n\t\t}, [value, focusedValue, orientation, variant, place]);\n\n\t\treturn (\n\t\t\t/*\n\t\t\t * role=\"tablist\" needs no accessible name of its own per the WAI-ARIA\n\t\t\t * Tabs pattern unless a page has more than one — a consumer with\n\t\t\t * several tab groups on screen at once can still add one through\n\t\t\t * `className` plus their own `aria-label` via a wrapping element, or by\n\t\t\t * giving each Tabs root a distinct heading before it.\n\t\t\t */\n\t\t\t<div\n\t\t\t\tref={setListRef}\n\t\t\t\trole=\"tablist\"\n\t\t\t\taria-orientation={orientation === \"vertical\" ? \"vertical\" : undefined}\n\t\t\t\tdata-orientation={orientation}\n\t\t\t\tdata-variant={variant}\n\t\t\t\tclassName={classes}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t{/*\n\t\t\t\t * Last child so it paints under the triggers in the segmented\n\t\t\t\t * variant without needing a stacking context on the list itself.\n\t\t\t\t * `aria-hidden` plus no role and no tabindex: it is never announced,\n\t\t\t\t * never focusable, and `pointer-events: none` keeps it out of the\n\t\t\t\t * hit-test entirely.\n\t\t\t\t */}\n\t\t\t\t<div\n\t\t\t\t\tref={indicatorRef}\n\t\t\t\t\tclassName=\"ft-tabs-indicator\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\tdata-variant={variant}\n\t\t\t\t\tdata-orientation={orientation}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t);\n\t}\n);\n\nTabsList.displayName = \"TabsList\";\n","type":"registry:ui","target":"@ui/fancy/components/tabs/TabsList.tsx"},{"path":"react/src/components/tabs/TabsTrigger.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 { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { TABS_KEY } from \"./types\";\nimport \"./tabs-trigger.css\";\n\ninterface TabsTriggerOwnProps {\n\t/** This trigger's value — which `TabsContent` it activates. */\n\tvalue: string;\n\t/** Disables just this trigger. A disabled trigger is skipped by the arrows and Home/End. */\n\tdisabled?: boolean;\n\t/** The trigger's content, typically the tab's 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 tab; the managed\n * ARIA, state and structural attributes stay the component's.\n */\nexport interface TabsTriggerProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"button\">, keyof TabsTriggerOwnProps>,\n\t\tTabsTriggerOwnProps {}\n\n/**\n * One tab in a `TabsList`.\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 TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(\n\t(\n\t\t{ value, disabled = false, children, className, onClick, onFocus, onBlur, onKeyDown, ...rest },\n\t\tforwardedRef\n\t) => {\n\t\t// Undefined outside a Tabs root: the trigger 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 — same degradation\n\t\t// as ToggleGroupItem/RadioGroupItem.\n\t\tconst context = useContext(TABS_KEY);\n\n\t\t// The click handler moves DOM focus to this very button, so the component\n\t\t// 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;\n\t\tconst isSelected = context?.isSelected(value) ?? false;\n\t\tconst variant = context?.variant ?? \"underline\";\n\t\tconst orientation = context?.orientation ?? \"horizontal\";\n\t\t// `undefined` outside a Tabs root leaves the native default (a plain\n\t\t// button is already in the tab order on its own); inside one, exactly\n\t\t// the trigger holding the roving position gets 0 and every other gets -1.\n\t\tconst tabIndexAttr = context ? (context.focusedValue === value ? 0 : -1) : undefined;\n\n\t\t// Geometry, not just colour, branches on variant: the mockup gives the\n\t\t// segmented pill a tighter box than the underline tab — 6px/12px versus\n\t\t// 8px/13px — not merely a different background.\n\t\t//\n\t\t// Neither branch paints the selection's *shape* any more: the segmented\n\t\t// pill's fill and the underline bar are both drawn by `TabsList`'s single\n\t\t// sliding indicator, which can travel between triggers in a way a\n\t\t// per-trigger background never could. What stays here is the part a\n\t\t// screen reader and a forced-colors user rely on — `aria-selected` below,\n\t\t// and the selected trigger's own foreground colour.\n\t\t//\n\t\t// `ft-tabs-trigger-selected` carries no rules of its own now. It stays in\n\t\t// the class string because it is a published styling hook, and dropping it\n\t\t// would silently break any consumer targeting it.\n\t\tconst classes = cn(\n\t\t\t\"ft-tabs-trigger inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap px-[14px] font-medium transition-colors\",\n\t\t\t\"focus-visible:outline-none\",\n\t\t\t\"disabled:pointer-events-none disabled:opacity-50\",\n\t\t\tvariant === \"segmented\"\n\t\t\t\t? cn(\n\t\t\t\t\t\t\"rounded-md py-[6px] text-[12px]\",\n\t\t\t\t\t\tisSelected ? \"text-accent-foreground\" : \"text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t)\n\t\t\t\t: cn(\n\t\t\t\t\t\t\"py-2 text-[13px]\",\n\t\t\t\t\t\tisSelected\n\t\t\t\t\t\t\t? \"ft-tabs-trigger-selected text-foreground\"\n\t\t\t\t\t\t\t: \"text-muted-foreground hover:text-foreground\"\n\t\t\t\t\t),\n\t\t\tclassName\n\t\t);\n\n\t\t// Whether this trigger holds real DOM focus right now. Refs, not state —\n\t\t// only the two effects below ever read them, and nothing needs to\n\t\t// re-render when they change.\n\t\tconst hasDomFocusRef = useRef(false);\n\t\t// Tracks the disabled transition across renders so the effect below can\n\t\t// tell \"just became disabled\" apart from \"already was\" or \"just became\n\t\t// enabled\".\n\t\tconst wasDisabledRef = useRef(false);\n\t\t// Set by the capture effect below, consumed by the reclaim effect below\n\t\t// it — the bridge the Svelte source builds with `$effect.pre`.\n\t\tconst reclaimPendingRef = useRef(false);\n\n\t\t// The Svelte source captures `document.activeElement` in an `$effect.pre`,\n\t\t// which runs BEFORE the DOM patch for that flush: in a real browser,\n\t\t// setting the native `disabled` attribute on a focused control forces an\n\t\t// immediate blur as part of applying that same patch, so a post-patch read\n\t\t// would see `<body>` and the reclaim below would never fire. React has no\n\t\t// pre-mutation phase for a function component, so the sample is kept\n\t\t// continuously instead — `handleFocus`/`handleBlur` maintain it, and\n\t\t// `handleBlur` deliberately ignores the one blur that arrives with\n\t\t// `disabled` already applied, because that blur IS the forced one this\n\t\t// branch exists for. jsdom reproduces neither the forced blur nor that\n\t\t// event (see `internals/menu.ts`'s note on the same limitation), which is\n\t\t// why `document.activeElement` is still consulted as well: between them\n\t\t// the two terms are correct in a browser and under jsdom alike, and\n\t\t// neither can produce a false positive.\n\t\tuseIsomorphicLayoutEffect(() => {\n\t\t\tconst nowDisabled = isDisabled;\n\t\t\tconst justDisabled = nowDisabled && !wasDisabledRef.current;\n\t\t\twasDisabledRef.current = nowDisabled;\n\t\t\tif (!justDisabled) return;\n\t\t\tconst node = innerRef.current;\n\t\t\treclaimPendingRef.current =\n\t\t\t\thasDomFocusRef.current || (node !== null && document.activeElement === node);\n\t\t\thasDomFocusRef.current = false;\n\t\t}, [isDisabled]);\n\n\t\t// Joins the roving-focus order whenever this trigger is enabled, and\n\t\t// leaves it in every other case: disabled from the start, or going\n\t\t// disabled mid-session. The effect's cleanup — run on unmount, and again\n\t\t// before each re-run when `value` or `isDisabled` changes — unregisters\n\t\t// 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) on\n\t\t// every root render. `register`/`unregister` are identity-stable by\n\t\t// contract for exactly this reason.\n\t\tconst register = context?.register;\n\t\tconst unregister = context?.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// This trigger just became disabled while it held real DOM focus. Hand\n\t\t// focus to whichever trigger inherits the roving position, guarded so a\n\t\t// trigger that never had focus can never steal it from wherever the user\n\t\t// actually is.\n\t\t//\n\t\t// It waits for `focusedValue` to stop naming this very trigger, which is\n\t\t// the observable that says the registry has already dropped it: the\n\t\t// unregister above is a state update, so it lands one render later than\n\t\t// the commit that disabled this button — where the Svelte source gets the\n\t\t// same ordering for free, because its cleanup runs in a pass that has\n\t\t// already completed. Nothing in this branch writes that registry itself.\n\t\t//\n\t\t// Once the registry has answered, the flag is consumed BEFORE the answer\n\t\t// is acted on — the Svelte source clears `hadFocusBeforeDisabling`\n\t\t// unconditionally, ahead of its own `next !== null` test. Leaving it\n\t\t// armed through an empty registry would let a later re-enable (which\n\t\t// makes `focusedValue` name a trigger again) fire this effect long after\n\t\t// the user has moved focus somewhere else entirely, dragging it back into\n\t\t// the tablist. The Svelte source cannot reach that state; neither may\n\t\t// this one.\n\t\tconst focusedValue = context?.focusedValue ?? null;\n\t\tconst focusElement = context?.focusElement;\n\t\tuseEffect(() => {\n\t\t\tif (!reclaimPendingRef.current || !isDisabled || !focusElement) return;\n\t\t\t// Still the roving position: the registry has not dropped this value\n\t\t\t// yet, so there is no inheritor to name. Keep waiting.\n\t\t\tif (focusedValue === value) return;\n\t\t\treclaimPendingRef.current = false;\n\t\t\tif (focusedValue === null) return;\n\t\t\tfocusElement(focusedValue);\n\t\t}, [isDisabled, focusedValue, focusElement, value]);\n\n\t\t// The native `disabled` attribute below is the real gate, but a synthetic\n\t\t// click fired straight at the element — as a test does — walks straight\n\t\t// past it, so the handler repeats the guard itself.\n\t\tfunction handleClick(event: MouseEvent<HTMLButtonElement>) {\n\t\t\tif (isDisabled) return;\n\t\t\tcontext?.select(value);\n\t\t\t// A native <button> is only guaranteed to take focus on click in some\n\t\t\t// browsers (macOS Safari notably does not, by default), so the roving\n\t\t\t// tab stop is moved here explicitly rather than left to an incidental\n\t\t\t// focus event — the same reasoning as ToggleGroupItem's click handler.\n\t\t\tcontext?.focus(value);\n\t\t\tinnerRef.current?.focus();\n\t\t\tonClick?.(event);\n\t\t}\n\n\t\t// Keeps the roving tabindex following real DOM focus even when focus\n\t\t// arrives some other way than this trigger's own click/keydown handlers\n\t\t// below — Shift+Tab back out of the panel, for instance.\n\t\tfunction handleFocus(event: FocusEvent<HTMLButtonElement>) {\n\t\t\thasDomFocusRef.current = true;\n\t\t\tif (isDisabled) return;\n\t\t\tcontext?.focus(value);\n\t\t\tonFocus?.(event);\n\t\t}\n\n\t\tfunction handleBlur(event: FocusEvent<HTMLButtonElement>) {\n\t\t\t// A browser force-blurs a focused control the instant its `disabled`\n\t\t\t// attribute is set, and that blur arrives with the attribute already\n\t\t\t// applied. It is precisely the case the reclaim above exists for, so it\n\t\t\t// must not clear the flag; every other blur does.\n\t\t\tif (!event.currentTarget.disabled) hasDomFocusRef.current = false;\n\t\t\tonBlur?.(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 (!context) return;\n\t\t\tconst horizontal = context.orientation === \"horizontal\";\n\t\t\tswitch (event.key) {\n\t\t\t\tcase \"ArrowRight\":\n\t\t\t\t\tif (!horizontal) return;\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tcontext.move(value, 1);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowLeft\":\n\t\t\t\t\tif (!horizontal) return;\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tcontext.move(value, -1);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowDown\":\n\t\t\t\t\tif (horizontal) return;\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tcontext.move(value, 1);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowUp\":\n\t\t\t\t\tif (horizontal) return;\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tcontext.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\tcontext.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\tcontext.moveToEdge(\"last\");\n\t\t\t\t\tbreak;\n\t\t\t\t// Enter/Space need no case here: a native <button> already fires a\n\t\t\t\t// click for both, and handleClick selects — in both activation\n\t\t\t\t// modes, since manual activation only withholds selection from the\n\t\t\t\t// *arrow* keys, not from an explicit activation key.\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\trole=\"tab\"\n\t\t\t\tdata-ft-tabs-trigger=\"\"\n\t\t\t\tdata-value={value}\n\t\t\t\tdata-orientation={orientation}\n\t\t\t\tdata-variant={variant}\n\t\t\t\tid={context?.triggerId(value) ?? rest.id}\n\t\t\t\tclassName={classes}\n\t\t\t\tdisabled={isDisabled}\n\t\t\t\taria-selected={isSelected}\n\t\t\t\taria-controls={context?.panelId(value) ?? rest[\"aria-controls\"]}\n\t\t\t\ttabIndex={tabIndexAttr ?? rest.tabIndex}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tonFocus={handleFocus}\n\t\t\t\tonBlur={handleBlur}\n\t\t\t\tonKeyDown={handleKeydown}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</button>\n\t\t);\n\t}\n);\n\nTabsTrigger.displayName = \"TabsTrigger\";\n","type":"registry:ui","target":"@ui/fancy/components/tabs/TabsTrigger.tsx"},{"path":"react/src/components/tabs/index.ts","content":"\"use client\";\n\nexport { Tabs } from \"./Tabs\";\nexport type { TabsProps } from \"./Tabs\";\nexport { TabsList } from \"./TabsList\";\nexport type { TabsListProps } from \"./TabsList\";\nexport { TabsTrigger } from \"./TabsTrigger\";\nexport type { TabsTriggerProps } from \"./TabsTrigger\";\nexport { TabsContent } from \"./TabsContent\";\nexport type { TabsContentProps } from \"./TabsContent\";\nexport { TABS_KEY } from \"./types\";\nexport type { TabsContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/tabs/index.ts"},{"path":"react/src/components/tabs/tabs-list.css","content":"/*\n * Makes the list the triggers' `offsetParent`, so `offsetLeft`/`offsetTop`\n * and the indicator's own `left: 0; top: 0` share one origin (both are\n * measured from the list's padding box). That shared origin is what\n * removes every fudge factor from the arithmetic in `TabsList.tsx` —\n * including the segmented rail's 3px padding, which simply falls out of\n * `offsetLeft`.\n *\n * `.ft-tabs-list` is the Svelte source's own root class, already the first\n * token of the root class list, so every rule below is anchored exactly as\n * the compiler-scoped originals were.\n */\n.ft-tabs-list {\n\tposition: relative;\n}\n\n/*\n * Exact rail padding/gap/radius from the mockup — not expressible as a\n * single Tailwind utility. Same `bg-background` reasoning as ToggleGroup's\n * rail: this app's dark theme has `--muted` *lighter* than `--card`, so a\n * muted fill on a card-nested rail would read as raised, the opposite of the\n * mockup's recessed strip.\n */\n.ft-tabs-list-segmented {\n\tborder-radius: 0.5rem; /* 8px */\n\tpadding: 3px;\n\tgap: 2px;\n}\n\n.ft-tabs-indicator {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tpointer-events: none;\n\t/* Hidden until the first successful measurement, so a not-yet-laid-out\n\t   first paint shows nothing rather than a sliver at the origin. */\n\topacity: 0;\n\ttransform-origin: left top;\n}\n\n/*\n * Underline, horizontal: a 2px bar pinned to the list's bottom edge and\n * grown along x only. `bottom: 0` overrides the base `top: 0` because the\n * height is explicit. This lands on exactly the pixels the retired\n * `inset 0 -2px 0` box-shadow on the trigger used to paint — the trigger's\n * bottom edge and the list's padding-box bottom are the same line.\n */\n.ft-tabs-indicator[data-variant=\"underline\"][data-orientation=\"horizontal\"] {\n\ttop: auto;\n\tbottom: 0;\n\twidth: 1px;\n\theight: 2px;\n\tbackground: var(--ft-tabs-indicator-color, var(--ft-nav-accent));\n\ttransform-origin: left bottom;\n}\n\n/* Underline, vertical: a 2px bar on the list's inline-start edge, grown\n   along y — where `inset 2px 0 0` used to sit, for the same reason. */\n.ft-tabs-indicator[data-variant=\"underline\"][data-orientation=\"vertical\"] {\n\twidth: 2px;\n\theight: 1px;\n\tbackground: var(--ft-tabs-indicator-color, var(--ft-nav-accent));\n\ttransform-origin: left top;\n}\n\n/*\n * Segmented: the pill itself, sized to the trigger's whole box on both\n * axes. Two variables carry the retired utilities' own values rather than\n * new numbers, so the resting look is unchanged: `--color-accent` is what\n * `bg-accent` resolved to, and `--radius-md` is what `rounded-md` resolved\n * to (Tailwind's stock `0.375rem` is only the last-resort literal — a\n * theme that redefines the scale, as this app's does, must be followed).\n *\n * The corner is then pre-divided by the scale factors JS writes, so what\n * renders after `scale()` has stretched a 1x1 box is the intended radius\n * rather than an ellipse — CSS `border-radius` takes a\n * `<horizontal> / <vertical>` pair for exactly this.\n */\n.ft-tabs-indicator[data-variant=\"segmented\"] {\n\twidth: 1px;\n\theight: 1px;\n\tbackground: var(--ft-tabs-indicator-color, var(--color-accent));\n\tborder-radius: calc(\n\t\t\tvar(--ft-tabs-indicator-radius, var(--radius-md, 0.375rem)) / var(--ft-tabs-indicator-sx, 1)\n\t\t) /\n\t\tcalc(\n\t\t\tvar(--ft-tabs-indicator-radius, var(--radius-md, 0.375rem)) / var(--ft-tabs-indicator-sy, 1)\n\t\t);\n\tz-index: 0;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-tabs-indicator {\n\t\t/* 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) =\n\t\t   tokens.EASINGS.inout. `inout`, not `out`: selecting a tab is a\n\t\t   reversible flip between two resting places, not an arrival. */\n\t\ttransition: transform var(--ft-tabs-indicator-duration, var(--ft-duration-fast, 150ms))\n\t\t\tvar(--ft-tabs-indicator-ease, var(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1)));\n\t}\n}\n\n/*\n * A forced-colors palette replaces every author background with a system\n * colour, so the fill has to be re-stated as one of that palette's own\n * keywords or the bar disappears. Repeating both attributes matches the\n * specificity of the most specific variant rule above, so this later\n * declaration actually wins rather than losing to it.\n */\n@media (forced-colors: active) {\n\t.ft-tabs-indicator[data-variant][data-orientation] {\n\t\tbackground: Highlight;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/tabs/tabs-list.css"},{"path":"react/src/components/tabs/tabs-trigger.css","content":"/*\n * Lifts every trigger above `TabsList`'s indicator, which sits at\n * `z-index: 0` in the same stacking context. Only the segmented variant\n * actually overlaps — the pill is painted under the label it belongs to —\n * but the rule is unconditional so a consumer restyling the underline\n * variant into something that overlaps cannot fall through the floor.\n *\n * `.ft-tabs-trigger` is the Svelte source's own root class, already the first\n * token of the root class list, so every rule below is anchored exactly as\n * the compiler-scoped originals were.\n */\n.ft-tabs-trigger {\n\tposition: relative;\n\tz-index: 1;\n}\n\n/*\n * The only `box-shadow` left on this element. It used to have to be\n * composited with an `inset` accent bar, because two `box-shadow` rules on\n * one element cannot both apply — the more specific simply wins and the\n * other disappears. The bar now lives on `TabsList`'s indicator, so the\n * focus ring stands alone and is never part of any transition: R13, no\n * animated focus.\n */\n.ft-tabs-trigger:focus-visible {\n\tbox-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-nav-accent) 35%, transparent);\n}\n\n/*\n * The segmented variant's selection is now painted by `TabsList`'s\n * indicator, which repaints as `Highlight` in a forced-colors palette and\n * sits *under* this label — whose own colour that same palette forces\n * independently, to `ButtonText`. `HighlightText` is the partner the\n * palette guarantees contrast against, and a system-colour keyword\n * declared inside the query is honoured rather than re-forced, which is\n * what makes re-stating it work at all (same mechanism as the indicator's\n * own fill, and as `Skeleton`/`TextRoll` elsewhere in the library).\n *\n * The underline variant needs nothing here: its bar sits on the list's\n * edge, not under the label, so the forced `ButtonText` stays legible.\n */\n@media (forced-colors: active) {\n\t.ft-tabs-trigger[data-variant=\"segmented\"][aria-selected=\"true\"] {\n\t\tcolor: HighlightText;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/tabs/tabs-trigger.css"},{"path":"react/src/components/tabs/tabs.css","content":"/*\n * Declared once, here, on the compound's actual root — TabsList and\n * TabsTrigger both read `var(--ft-nav-accent)` without redeclaring the\n * fallback. Custom-property inheritance is what carries it to both of them in\n * the live DOM, exactly as it did when the Svelte compiler scoped this rule.\n *\n * `.ft-tabs` is the Svelte source's own root class, already the first token of\n * the root class list, so the rule below is anchored on it exactly as the\n * compiler-scoped original was.\n */\n.ft-tabs {\n\t--ft-nav-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/tabs/tabs.css"},{"path":"react/src/components/tabs/types.ts","content":"\"use client\";\n\n/**\n * The contract between the tabs root and its list/trigger/content pieces.\n *\n * `Tabs` owns the active value and the roving-focus position; `TabsTrigger`\n * and `TabsContent` read both through context instead of a prop and a\n * callback threaded down by hand for each of them.\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\n * triggers that mount out of order, or that get reordered by a keyed list\n * after mounting, still step through in the order they actually appear on\n * screen. Registration order is used only as the pre-interaction fallback\n * for which trigger starts out tabbable — the same split `ToggleGroup` uses,\n * for the same reason.\n */\n\nimport { createContext } from \"react\";\n\n/** What the root publishes. `TabsTrigger`/`TabsContent` read it; only the root writes it. */\nexport interface TabsContext {\n\t/** The active tab's value. */\n\treadonly value: string;\n\t/** The tablist's stacking axis and which arrow-key pair moves it. */\n\treadonly orientation: \"horizontal\" | \"vertical\";\n\t/** Whether arrowing to a trigger selects it immediately, or only moves focus. */\n\treadonly activation: \"automatic\" | \"manual\";\n\t/** Visual treatment, published so both `TabsList` and `TabsTrigger` render in step. */\n\treadonly variant: \"underline\" | \"segmented\";\n\t/** Whether `itemValue` is the active tab. */\n\tisSelected(itemValue: string): boolean;\n\t/** Makes `itemValue` the active tab and fires the root's `onValueChange`. */\n\tselect(itemValue: string): void;\n\t/**\n\t * Joins the roving-focus order. A trigger skips this entirely while disabled.\n\t *\n\t * Identity-stable for the life of the root: it is called from a trigger's own\n\t * effect and therefore appears in that effect's dependency list. This is the\n\t * React counterpart of the `untrack` the Svelte source wraps the body in —\n\t * there, an effect that reads the registry it just mutated alternates between\n\t * register and unregister until the scheduler gives up; here, a context member\n\t * whose identity changed on every root render would re-run the same effect on\n\t * every root render, unregistering and re-registering forever. The state\n\t * updater is also a no-op returning the *same* array when nothing changes, so\n\t * 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 triggers and wrapping at both ends, and moves DOM\n\t * focus along with it. With `activation=\"automatic\"` this also selects\n\t * the trigger it lands on.\n\t */\n\tmove(from: string, delta: number): void;\n\t/**\n\t * Moves the roving-focus position, and DOM focus, to the first or last\n\t * enabled trigger in DOM order. With `activation=\"automatic\"` this also\n\t * selects it.\n\t */\n\tmoveToEdge(edge: \"first\" | \"last\"): void;\n\t/**\n\t * Moves real DOM focus to `itemValue`'s trigger, if it is currently\n\t * registered and enabled, and updates the roving-focus position to\n\t * match. Distinct from `focus()`, which only updates the roving-focus\n\t * position without touching DOM focus: this exists for the one case\n\t * where DOM focus itself has to follow, because the element that held\n\t * it just left the roving order — typically because it just became\n\t * disabled out from under a keyboard user.\n\t *\n\t * Identity-stable, for the same reason as `register`: a trigger reads it\n\t * from an effect's dependency list.\n\t */\n\tfocusElement(itemValue: string): void;\n\t/** The id a `TabsTrigger` for `itemValue` renders, and the matching `TabsContent`'s `aria-labelledby` target. */\n\ttriggerId(itemValue: string): string;\n\t/** The id a `TabsContent` for `itemValue` renders, and the matching `TabsTrigger`'s `aria-controls` target. */\n\tpanelId(itemValue: string): string;\n}\n\n/**\n * The context a list/trigger/panel reads to find its surrounding root. The\n * Svelte source publishes it under a `Symbol` context key; React's own context\n * object plays 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 tabs = useContext(TABS_KEY);\n * ```\n *\n * Read it as optional — a piece rendered outside a `Tabs` root gets\n * `undefined` rather than throwing.\n */\nexport const TABS_KEY = createContext<TabsContext | undefined>(undefined);\nTABS_KEY.displayName = \"TabsContext\";\n","type":"registry:ui","target":"@ui/fancy/components/tabs/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/tabs?framework=react"}}