{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","type":"registry:ui","title":"Stepper","description":"A multi-step progress indicator on light rails: each completed step sends a single sweep of light down the rail to the next, the current bullet breathes a soft halo, and each check draws itself.","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/stepper/Step.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { STEPPER_KEY } from \"./types\";\nimport \"./step.css\";\n\nexport interface StepProps {\n\t/**\n\t * The step's primary label. Required: with `children` also omitted, a\n\t * clickable step's only accessible text is its `sr-only` status span —\n\t * every upcoming step in the same `Stepper` would then read as the\n\t * identical \"not started, button\" with nothing to tell them apart.\n\t */\n\tlabel: string;\n\t/** Optional secondary line shown under the label. */\n\tdescription?: string;\n\t/** Overrides the bullet's default content (checkmark / number / outline). */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\ntype StepStatus = \"done\" | \"current\" | \"upcoming\";\n\nconst STATUS_TEXT: Record<StepStatus, string> = {\n\tdone: \"completed\",\n\tcurrent: \"current step\",\n\tupcoming: \"not started\",\n};\n\n/**\n * One stop on a `Stepper`'s rail. Its number and status come from its\n * position among its registered siblings, never from a prop.\n *\n * The root element arrives through the ref channel rather than a `ref`\n * prop, per PORTING.md — the Svelte source declares `ref = $bindable(null)`.\n */\nexport const Step = forwardRef<HTMLLIElement, StepProps>(function Step(\n\t{ label, description, children, className },\n\tref\n) {\n\t// Undefined outside a Stepper: the step then has no shared index or\n\t// status to derive, and renders as a plain, always-\"upcoming\",\n\t// never-clickable item rather than throwing.\n\tconst stepper = useContext(STEPPER_KEY);\n\n\t// `useFancyId()` rather than `_internals/id.ts`'s `uid()`: this needs to\n\t// be stable and available immediately, including during SSR, and\n\t// `uid()` is client-only by design (see its own doc comment).\n\tconst id = useFancyId();\n\n\t// Registers on mount, unregisters on unmount. The effect depends on the\n\t// `register` function alone, never on the whole context object: the\n\t// context is rebuilt every time the registry changes, so depending on it\n\t// would re-run this effect as a result of this effect's own call — see\n\t// `Stepper.tsx` for the fuller account of the loop that guards against.\n\t//\n\t// A layout effect, not a passive one: every number, status colour,\n\t// connector and `aria-current` on the rail is derived from the index this\n\t// call settles, so a passive registration would let the browser paint one\n\t// frame in which every step still reads -1 — bullets numbered \"0\", a\n\t// leading connector on the first step, and no current step at all.\n\tconst register = stepper?.register;\n\tuseIsomorphicLayoutEffect(() => {\n\t\tif (!register) return;\n\t\treturn register(id);\n\t}, [register, id]);\n\n\tconst index = stepper ? stepper.indexOf(id) : -1;\n\n\tconst status: StepStatus = (() => {\n\t\tif (!stepper || index === -1) return \"upcoming\";\n\t\tif (index < stepper.current) return \"done\";\n\t\tif (index === stepper.current) return \"current\";\n\t\treturn \"upcoming\";\n\t})();\n\n\tconst orientation = stepper?.orientation ?? \"horizontal\";\n\tconst clickable = stepper?.clickable ?? false;\n\tconst isFirst = index === 0;\n\tconst isLast = stepper ? index === stepper.count - 1 : true;\n\n\t// A rail is the segment between two neighbouring steps, identified by the\n\t// index of the step it leaves. Horizontally a step draws the rail that\n\t// arrives at it (so the first step has none); vertically it draws the one\n\t// that leaves it (so the last step has none). Either way the rail is lit\n\t// exactly when the step it leaves is done.\n\tconst railFrom = orientation === \"vertical\" ? index : index - 1;\n\tconst connectorDone = stepper && index !== -1 ? railFrom < stepper.current : false;\n\n\tconst animate = stepper?.animate ?? false;\n\n\t// Order within one run of light, in beats. Moving forward from `origin`\n\t// (the previous active step), rail k lights `k - origin` beats after the\n\t// first one; moving back, rails retract from the far end first. Clamped at\n\t// 0: anything outside the run moves (if at all) immediately.\n\tconst origin = stepper?.origin ?? 0;\n\tconst forward = stepper ? stepper.current >= origin : true;\n\tconst railOrder = Math.max(0, forward ? railFrom - origin : origin - 1 - railFrom);\n\n\t// A step the light is travelling *to* (past the origin, up to and\n\t// including the new current one) settles when the light reaches it:\n\t// its incoming rail's beat, plus the part of a sweep it takes the head to\n\t// arrive. `undefined` leaves the CSS fallback (no delay) in charge.\n\tconst arrival =\n\t\tstepper && forward && index > origin && index <= stepper.current\n\t\t\t? index - origin - 1 + 0.6\n\t\t\t: undefined;\n\n\t// Custom properties are written as inline styles, the counterpart of the\n\t// Svelte source's `style:--ft-step-*` directives. An `undefined` arrival\n\t// leaves the attribute off entirely, as the directive does.\n\tconst railStyle = { \"--ft-step-order\": railOrder } as CSSProperties;\n\tconst bulletStyle =\n\t\tarrival === undefined ? undefined : ({ \"--ft-step-arrival\": arrival } as CSSProperties);\n\n\tfunction handleClick() {\n\t\tif (!stepper || !clickable || index === -1) return;\n\t\tstepper.select(index);\n\t}\n\n\tconst bulletClasses = cn(\n\t\t\"ft-step-bullet relative inline-flex size-7 shrink-0 items-center justify-center rounded-full text-[12px] font-semibold tabular-nums\",\n\t\tstatus === \"done\" && \"ft-step-bullet-done\",\n\t\tstatus === \"current\" && \"ft-step-bullet-current ft-step-bullet-halo\",\n\t\tstatus === \"upcoming\" && \"ft-step-bullet-upcoming text-muted-foreground\"\n\t);\n\n\t// `children ?? …` rather than a truthiness test: the Svelte side branches\n\t// on whether the snippet exists, and `null`/`undefined` are the only\n\t// React values that mean \"nothing was passed\".\n\tconst defaultBullet =\n\t\tstatus === \"done\" ? (\n\t\t\t<svg\n\t\t\t\tclassName=\"ft-step-glyph ft-step-check size-3.5\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2.75\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t<path d=\"M20 6 9 17l-5-5\" pathLength={1} />\n\t\t\t</svg>\n\t\t) : (\n\t\t\t<span className=\"ft-step-glyph\" aria-hidden=\"true\">\n\t\t\t\t{index + 1}\n\t\t\t</span>\n\t\t);\n\n\tconst bulletContent = (\n\t\t<span className={bulletClasses} data-status={status} style={bulletStyle}>\n\t\t\t{children ?? defaultBullet}\n\t\t\t<span className=\"sr-only\"> {STATUS_TEXT[status]}</span>\n\t\t</span>\n\t);\n\n\tconst textContent = (\n\t\t<span\n\t\t\tclassName={cn(\"flex flex-col\", orientation === \"horizontal\" && \"items-center text-center\")}\n\t\t>\n\t\t\t{label ? (\n\t\t\t\t<span\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"ft-step-label text-xs transition-colors\",\n\t\t\t\t\t\tstatus === \"current\" && \"text-foreground font-medium\",\n\t\t\t\t\t\tstatus === \"done\" && \"text-foreground/80\",\n\t\t\t\t\t\tstatus === \"upcoming\" && \"text-muted-foreground\"\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{label}\n\t\t\t\t</span>\n\t\t\t) : null}\n\t\t\t{description ? (\n\t\t\t\t<span className=\"ft-step-description text-muted-foreground/80 text-[11px] leading-snug\">\n\t\t\t\t\t{description}\n\t\t\t\t</span>\n\t\t\t) : null}\n\t\t</span>\n\t);\n\n\treturn (\n\t\t<li\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"ft-step flex\",\n\t\t\t\torientation === \"vertical\"\n\t\t\t\t\t? cn(\"flex-row items-stretch gap-3\", isLast ? \"pb-0\" : \"pb-7\")\n\t\t\t\t\t: cn(\"flex-col items-center\", isFirst ? \"flex-none\" : \"flex-1\"),\n\t\t\t\tanimate && \"ft-step-animate\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tdata-status={status}\n\t\t\tdata-orientation={orientation}\n\t\t\taria-current={status === \"current\" ? \"step\" : undefined}\n\t\t>\n\t\t\t{orientation === \"horizontal\" ? (\n\t\t\t\t<div className=\"flex w-full items-start\">\n\t\t\t\t\t{!isFirst && (\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"ft-step-connector bg-border relative mx-1.5 mt-[13px] h-0.5 flex-1 rounded-full\",\n\t\t\t\t\t\t\t\tconnectorDone && \"ft-step-connector-done ft-step-connector-lit\"\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tstyle={railStyle}\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{clickable ? (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"ft-step-trigger flex shrink-0 cursor-pointer flex-col items-center gap-1.5 px-1 focus-visible:outline-none\"\n\t\t\t\t\t\t\tonClick={handleClick}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{bulletContent}\n\t\t\t\t\t\t\t{textContent}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div className=\"ft-step-trigger flex shrink-0 flex-col items-center gap-1.5 px-1\">\n\t\t\t\t\t\t\t{bulletContent}\n\t\t\t\t\t\t\t{textContent}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t) : (\n\t\t\t\t<>\n\t\t\t\t\t{/*\n\t\t\t\t\t\t`-mb-7` mirrors the li's own `pb-7`: without it the bullet column\n\t\t\t\t\t\tstretches only to the li's content box, which is barely taller than\n\t\t\t\t\t\tthe bullet, so the rail's `flex-1` resolved to ~0px and the vertical\n\t\t\t\t\t\trail never showed. Reaching into the padding gives it the gap to span.\n\t\t\t\t\t*/}\n\t\t\t\t\t<div className={cn(\"flex flex-col items-center self-stretch\", !isLast && \"-mb-7\")}>\n\t\t\t\t\t\t{bulletContent}\n\t\t\t\t\t\t{!isLast && (\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"ft-step-connector bg-border relative my-1 w-0.5 flex-1 rounded-full\",\n\t\t\t\t\t\t\t\t\tconnectorDone && \"ft-step-connector-done ft-step-connector-lit\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tstyle={railStyle}\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t{clickable ? (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"ft-step-trigger flex cursor-pointer flex-col gap-0.5 pt-0.5 text-left focus-visible:outline-none\"\n\t\t\t\t\t\t\tonClick={handleClick}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{textContent}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div className=\"ft-step-trigger flex flex-col gap-0.5 pt-0.5 text-left\">\n\t\t\t\t\t\t\t{textContent}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t)}\n\t\t</li>\n\t);\n});\n\nStep.displayName = \"Step\";\n","type":"registry:ui","target":"@ui/fancy/components/stepper/Step.tsx"},{"path":"react/src/components/stepper/Stepper.tsx","content":"\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { useReducedMotion } from \"../../internals/motion/media-query\";\nimport { STEPPER_KEY } from \"./types\";\nimport type { StepperContext } from \"./types\";\n\nexport interface StepperProps {\n\t/**\n\t * The active step's 0-based index. Controlled when supplied: pair it\n\t * with `onCurrentChange`, the React counterpart of the Svelte source's\n\t * `bind:current`. Left out, the stepper keeps the index itself and\n\t * starts at 0.\n\t */\n\tcurrent?: number;\n\t/** Called with the new index whenever it changes, however the change happened. */\n\tonCurrentChange?: (current: number) => void;\n\t/** The rail's stacking axis. Defaults to `\"horizontal\"`. */\n\torientation?: \"horizontal\" | \"vertical\";\n\t/** Whether steps render as buttons a reader can click to jump between them. Defaults to `false`. */\n\tclickable?: boolean;\n\t/** Called with a step's index when it's activated by a click. Only fires when `clickable`. */\n\tonStepClick?: (index: number) => void;\n\t/** The `Step`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 rail a set of `Step`s reads its shared state from.\n *\n * The root element arrives through the ref channel rather than a `ref`\n * prop, 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 Stepper = forwardRef<HTMLOListElement, StepperProps>(function Stepper(\n\t{\n\t\tcurrent,\n\t\tonCurrentChange,\n\t\torientation = \"horizontal\",\n\t\tclickable = false,\n\t\tonStepClick,\n\t\tchildren,\n\t\tclassName,\n\t\tsound = false,\n\t},\n\tref\n) {\n\t// `current = $bindable(0)` on the Svelte side. A supplied prop wins and\n\t// the consumer owns the value; with nothing supplied the component owns\n\t// it, starting at the same 0 the Svelte default uses.\n\tconst [uncontrolledCurrent, setUncontrolledCurrent] = useState(0);\n\tconst isControlled = current !== undefined;\n\tconst activeIndex = isControlled ? current : uncontrolledCurrent;\n\n\t// Ids, not elements: a `Step` can register the instant its own effect\n\t// runs, with no need to wait on a ref to have landed first.\n\t//\n\t// The live registry is a ref, and the state array is the render-visible\n\t// copy of it. Both are needed: a `Step` registers from its own mount\n\t// effect, and React runs sibling effects back to back before it\n\t// re-renders anything, so the second sibling has to see the first\n\t// sibling's write immediately — a `setState` updater alone would work,\n\t// but `register` also has to *read* the current list synchronously to\n\t// refuse a duplicate and to hand back an unregister that splices the\n\t// right entry.\n\t//\n\t// `register` is wrapped in a `useCallback` with an empty dependency\n\t// list, and that is load-bearing rather than tidy: a `Step`'s\n\t// registration effect depends on it, so a `register` rebuilt whenever\n\t// the registry changes would make every step's effect re-run the\n\t// instant its own call mutated the registry — unregister, register,\n\t// re-render, unregister, forever. This is the React shape of the exact\n\t// bug the Svelte source's `untrack` calls guard against.\n\tconst registryRef = useRef<string[]>([]);\n\tconst [registered, setRegistered] = useState<string[]>([]);\n\n\tconst register = useCallback((id: string): (() => void) => {\n\t\tif (!registryRef.current.includes(id)) {\n\t\t\tregistryRef.current = [...registryRef.current, id];\n\t\t\tsetRegistered(registryRef.current);\n\t\t}\n\t\treturn () => {\n\t\t\tconst index = registryRef.current.indexOf(id);\n\t\t\tif (index !== -1) {\n\t\t\t\tconst next = [...registryRef.current];\n\t\t\t\tnext.splice(index, 1);\n\t\t\t\tregistryRef.current = next;\n\t\t\t\tsetRegistered(next);\n\t\t\t}\n\t\t};\n\t}, []);\n\n\t// Reads the rendered copy, not the ref: a `Step` calls this during its\n\t// own render, and the answer has to be the one this render pass was\n\t// scheduled for.\n\tconst indexOf = useCallback((id: string): number => registered.indexOf(id), [registered]);\n\n\t// Motion is opt-in on the client only: the server render (and the\n\t// hydration pass) paints the still composition, and the animated classes\n\t// arrive once the browser has actually been asked about\n\t// `prefers-reduced-motion` — `asked` flips in a mount effect, the\n\t// counterpart of the Svelte source's `reduced.start()` + `asked = true`.\n\t// Under reduce they never arrive, so the rails fill by colour alone.\n\tconst reduced = useReducedMotion();\n\tconst [asked, setAsked] = useState(false);\n\tuseEffect(() => {\n\t\tsetAsked(true);\n\t}, []);\n\tconst animate = asked && !reduced;\n\n\t// Where the light set off from: the active index *before* the latest\n\t// change. Steps read it to order the rail sweeps (and the arrival of the\n\t// bullets behind them) as one continuous run from the old step to the new\n\t// one, instead of every rail lighting in the same frame. It starts at 0 —\n\t// not at `current` — so the first paint plays the same run from the first\n\t// step, a one-time arrival over rails that are already filled.\n\t//\n\t// The Svelte source writes it from an `$effect.pre`, i.e. before the DOM\n\t// update that shows the new index. The React counterpart is the \"adjust\n\t// state while rendering\" pattern: when the index differs from the one last\n\t// settled, the pair is updated during this render, React re-renders before\n\t// committing, and no frame ever shows the new index with a stale origin.\n\tconst [run, setRun] = useState(() => ({ settled: activeIndex, origin: 0 }));\n\tlet origin = run.origin;\n\tif (run.settled !== activeIndex) {\n\t\torigin = run.settled;\n\t\tsetRun({ settled: activeIndex, origin: run.settled });\n\t}\n\n\tconst playCue = useSoundCue(sound);\n\n\tconst select = useCallback(\n\t\t(index: number) => {\n\t\t\tif (!clickable) return;\n\t\t\t// Changed-only, and ahead of every callback: re-picking the step that\n\t\t\t// is already current still reports through `onStepClick` and\n\t\t\t// `onCurrentChange`, but makes no sound — the rail did not move.\n\t\t\tif (activeIndex !== index) playCue(\"select\");\n\t\t\tonStepClick?.(index);\n\t\t\tif (!isControlled) setUncontrolledCurrent(index);\n\t\t\tonCurrentChange?.(index);\n\t\t},\n\t\t[clickable, isControlled, onStepClick, onCurrentChange, activeIndex, playCue]\n\t);\n\n\t// Rebuilt when any of its inputs actually changes — that rebuild is what\n\t// re-renders the steps reading it, and it is the React counterpart of the\n\t// Svelte context's live getters.\n\tconst context = useMemo<StepperContext>(\n\t\t() => ({\n\t\t\torientation,\n\t\t\tclickable,\n\t\t\tcurrent: activeIndex,\n\t\t\tcount: registered.length,\n\t\t\torigin,\n\t\t\tanimate,\n\t\t\tregister,\n\t\t\tindexOf,\n\t\t\tselect,\n\t\t}),\n\t\t[orientation, clickable, activeIndex, registered, origin, animate, register, indexOf, select]\n\t);\n\n\treturn (\n\t\t<STEPPER_KEY.Provider value={context}>\n\t\t\t{/*\n\t\t\t\t`role=\"list\"` is stated rather than left implicit: `list-style: none`\n\t\t\t\tstrips list semantics in Safari, and a step's position in the count is\n\t\t\t\tthe only thing telling a reader it is \"2 of 5\" — the visible number is\n\t\t\t\t`aria-hidden`.\n\t\t\t*/}\n\t\t\t<ol\n\t\t\t\tref={ref}\n\t\t\t\trole=\"list\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-stepper flex list-none\",\n\t\t\t\t\torientation === \"vertical\" ? \"flex-col\" : \"w-full items-start\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\tdata-orientation={orientation}\n\t\t\t\tdata-motion={animate ? \"full\" : \"reduced\"}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</ol>\n\t\t</STEPPER_KEY.Provider>\n\t);\n});\n\nStepper.displayName = \"Stepper\";\n\n/*\n  No colocated stylesheet here: the root `<ol>` itself never paints the brand\n  purple — only a `Step`'s current bullet, halo, and lit rail do — so\n  `--ft-nav-accent` is declared in `step.css` instead, the same split a toggle\n  group (no purple of its own) and its items (declaring their focus-ring accent\n  locally) already use.\n*/\n","type":"registry:ui","target":"@ui/fancy/components/stepper/Stepper.tsx"},{"path":"react/src/components/stepper/index.ts","content":"\"use client\";\n\nexport { Stepper } from \"./Stepper\";\nexport type { StepperProps } from \"./Stepper\";\nexport { Step } from \"./Step\";\nexport type { StepProps } from \"./Step\";\nexport { STEPPER_KEY } from \"./types\";\nexport type { StepperContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/stepper/index.ts"},{"path":"react/src/components/stepper/step.css","content":"/*\n * Port note: Svelte scoped these rules; plain CSS is global, so every\n * selector below is anchored on a class this component owns and puts on its\n * own elements (`.ft-step`, `.ft-step-bullet*`, `.ft-step-connector*`,\n * `.ft-step-glyph`, `.ft-step-check`, `.ft-step-trigger`, `.ft-step-animate`\n * — all of them the Svelte source's own names, already on the elements'\n * class lists). No anchor had to be added. Everything below is copied\n * verbatim from the Svelte source's <style> block.\n */\n\n/*\n * The only place this component reaches for the brand purple — the\n * current bullet's fill/halo and a lit rail. No semantic token owns it,\n * so it's declared locally with a light-dark() fallback, retintable from\n * higher up the tree via `--ft-accent`. Mirrors ToggleGroupItem's\n * identical `--ft-toggle-group-accent` pattern. Never redeclare\n * `--ft-accent` itself: that would shadow whatever an ancestor set, the\n * exact bug an earlier wave shipped in `Link`.\n *\n * Every private `--_x` reads a public `--ft-step-*` first, so a consumer\n * can retune the rail from a class without touching this file.\n */\n.ft-step {\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\t/* What a lit rail settles to: the accent at reduced strength. */\n\t--_rail: var(--ft-step-rail, color-mix(in oklab, var(--ft-nav-accent) 58%, transparent));\n\t/* The head of the travelling light. White-hot on dark; on a light\n\t * surface a white core would vanish, so it stays close to the accent. */\n\t--_hot: var(\n\t\t--ft-step-glint,\n\t\tlight-dark(\n\t\t\tcolor-mix(in oklab, var(--ft-nav-accent) 82%, white),\n\t\t\tcolor-mix(in oklab, var(--ft-nav-accent) 45%, white)\n\t\t)\n\t);\n\t--_sweep: var(--ft-step-sweep-duration, 420ms);\n\t/* One beat = how far behind the previous rail the next one lights, so\n\t * a multi-step jump reads as a single run of light. */\n\t--_beat: calc(var(--_sweep) * 0.7);\n\t--_ease-out: var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));\n}\n\n/*\n * A step's whole visible state — the bullet's fill, its label colour, the\n * halo around the current one, and the rail behind it — eases in on the\n * same clock as everything else in the library.\n *\n * Declared outside any `prefers-reduced-motion` query on purpose: none of\n * these properties moves anything. A colour that crossfades and a static\n * ring that appears are state changes, not travel, and suppressing them\n * under reduced motion would make the stepper flicker rather than settle.\n * The focus ring is safe from this list because it lives on a different\n * element (`.ft-step-trigger`), so no `box-shadow` here is ever a focus\n * indicator.\n *\n * 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) = tokens.EASINGS.inout\n */\n.ft-step-bullet,\n.ft-step-connector,\n.ft-step-connector::before {\n\t--ft-step-signal: var(--ft-step-signal-duration, var(--ft-duration-fast, 150ms))\n\t\tvar(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));\n\ttransition:\n\t\tbackground-color var(--ft-step-signal),\n\t\tcolor var(--ft-step-signal),\n\t\tbox-shadow var(--ft-step-signal);\n}\n\n/* ---------------------------------------------------------------- bullets */\n\n/*\n * The outline is an inset box-shadow, not a border: it then rides the\n * same (possibly delayed) transition as the fill, so a step the light is\n * still travelling towards keeps its ring until the light lands instead\n * of dropping it the instant `current` changes.\n */\n.ft-step-bullet-upcoming {\n\tbackground: color-mix(in oklab, currentColor 5%, transparent);\n\tbox-shadow: inset 0 0 0 1px\n\t\tvar(--ft-step-outline, var(--border, color-mix(in oklab, currentColor 22%, transparent)));\n}\n\n.ft-step-bullet-done {\n\tbackground: var(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)));\n\tcolor: light-dark(oklch(1 0 0), oklch(0.15 0 0));\n\tbox-shadow:\n\t\tinset 0 1px 0 rgb(255 255 255 / 0.22),\n\t\t0 1px 2px rgb(0 0 0 / 0.12);\n}\n\n.ft-step-bullet-current {\n\tbackground: var(--ft-nav-accent);\n\tcolor: light-dark(oklch(1 0 0), oklch(0.15 0 0));\n\tbox-shadow:\n\t\tinset 0 1px 0 rgb(255 255 255 / 0.28),\n\t\t0 1px 2px rgb(0 0 0 / 0.14);\n}\n\n/*\n * The halo lives on its own layer so it can breathe through `opacity`\n * alone — never re-painting a box-shadow per frame, never scaling. At\n * rest (SSR, reduced motion) it is simply on.\n */\n.ft-step-bullet-halo::before {\n\tcontent: \"\";\n\tposition: absolute;\n\tinset: 0;\n\tborder-radius: inherit;\n\tpointer-events: none;\n\tbox-shadow:\n\t\t0 0 0 4px color-mix(in oklab, var(--ft-nav-accent) 18%, transparent),\n\t\t0 0 18px 2px color-mix(in oklab, var(--ft-nav-accent) 30%, transparent);\n}\n\n.ft-step-glyph {\n\tdisplay: inline-block;\n}\n\n/* ------------------------------------------------------------------ rails */\n\n/*\n * The rail is a hairline track (the connector itself, `bg-border`) with\n * two layers on top: `::before`, the settled fill, and `::after`, the\n * light that travels once across it when it lights.\n *\n * At rest the fill is full-length and only its colour changes, so the\n * no-motion path is a pure cross-fade.\n */\n.ft-step-connector {\n\t/* Horizontal clip only — the travelling light may glow above and below\n\t * the rail but never spill past its ends onto a bullet. */\n\tclip-path: inset(-10px 0);\n}\n.ft-step[data-orientation=\"vertical\"] .ft-step-connector {\n\tclip-path: inset(0 -10px);\n}\n\n.ft-step-connector::before,\n.ft-step-connector::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tpointer-events: none;\n\tborder-radius: inherit;\n}\n\n.ft-step-connector::before {\n\tinset: 0;\n\tbackground-color: transparent;\n}\n.ft-step-connector-lit::before {\n\tbackground-color: var(--_rail);\n}\n\n.ft-step-connector::after {\n\ttop: 0;\n\tbottom: 0;\n\tleft: 0;\n\twidth: 45%;\n\topacity: 0;\n\tbackground: linear-gradient(\n\t\t90deg,\n\t\ttransparent,\n\t\tcolor-mix(in oklab, var(--ft-nav-accent) 55%, transparent) 45%,\n\t\tvar(--ft-nav-accent) 78%,\n\t\tvar(--_hot) 97%,\n\t\tvar(--_hot)\n\t);\n\tfilter: drop-shadow(0 0 3px color-mix(in oklab, var(--ft-nav-accent) 80%, transparent));\n}\n.ft-step[data-orientation=\"vertical\"] .ft-step-connector::after {\n\tright: 0;\n\twidth: auto;\n\theight: 45%;\n\tbottom: auto;\n\tbackground: linear-gradient(\n\t\t180deg,\n\t\ttransparent,\n\t\tcolor-mix(in oklab, var(--ft-nav-accent) 55%, transparent) 45%,\n\t\tvar(--ft-nav-accent) 78%,\n\t\tvar(--_hot) 97%,\n\t\tvar(--_hot)\n\t);\n}\n\n.ft-step-trigger:focus-visible {\n\toutline: none;\n\tbox-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-nav-accent) 35%, transparent);\n\tborder-radius: 0.375rem;\n}\n\n/* ----------------------------------------------------------------- motion */\n\n/*\n * Everything below travels, so it is doubly gated: the OS preference\n * (this query) and `.ft-step-animate`, which the root only hands out on\n * the client once it has read the same preference. The resting state\n * above is the fallback for both.\n *\n * `--ft-step-order` (rails) and `--ft-step-arrival` (bullets) are written\n * inline by `Step` — beats since the light set off from the previous\n * active step, so one jump across several steps lights them in sequence.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t/* The fill grows along the rail instead of cross-fading, and retracts\n\t * the same way when the reader steps back. An unlit fill stays\n\t * transparent while collapsed: `.ft-step-animate` lands on elements\n\t * already painted (hydration, a post-paint effect, the OS preference\n\t * flipping), and a fill that turned the rail colour in that same style\n\t * change would show every unlit rail fully lit for its delay, then\n\t * retract. Its colour only drops once a retraction has finished\n\t * (a 0s transition delayed past the sweep); lighting switches it on\n\t * instantly while the fill is still collapsed. */\n\t.ft-step-animate .ft-step-connector::before {\n\t\ttransform: scaleX(0);\n\t\ttransform-origin: left center;\n\t\ttransition:\n\t\t\ttransform var(--_sweep) var(--_ease-out) calc(var(--ft-step-order, 0) * var(--_beat)),\n\t\t\tbackground-color 0s linear calc(var(--ft-step-order, 0) * var(--_beat) + var(--_sweep));\n\t}\n\t.ft-step-animate[data-orientation=\"vertical\"] .ft-step-connector::before {\n\t\ttransform: scaleY(0);\n\t\ttransform-origin: center top;\n\t}\n\t.ft-step-animate .ft-step-connector-lit::before,\n\t.ft-step-animate[data-orientation=\"vertical\"] .ft-step-connector-lit::before {\n\t\ttransform: none;\n\t\ttransition: transform var(--_sweep) var(--_ease-out)\n\t\t\tcalc(var(--ft-step-order, 0) * var(--_beat));\n\t}\n\n\t/* The light: a comet whose head rides exactly on the fill's leading\n\t * edge (same duration, same curve, translate range chosen so head =\n\t * fill edge), then fades once it has arrived. Keyed on the lit class,\n\t * so it plays once each time a rail lights — and once on first paint,\n\t * as the arrival. */\n\t.ft-step-animate .ft-step-connector-lit::after {\n\t\tanimation:\n\t\t\tft-step-sweep-x var(--_sweep) var(--_ease-out) calc(var(--ft-step-order, 0) * var(--_beat))\n\t\t\t\tboth,\n\t\t\tft-step-sweep-fade calc(var(--_sweep) * 0.7) linear\n\t\t\t\tcalc(var(--ft-step-order, 0) * var(--_beat) + var(--_sweep) * 0.6) both;\n\t}\n\t.ft-step-animate[data-orientation=\"vertical\"] .ft-step-connector-lit::after {\n\t\tanimation-name: ft-step-sweep-y, ft-step-sweep-fade;\n\t}\n\n\t/* A bullet the light is heading for settles when it gets there, not\n\t * the instant `current` changes. */\n\t.ft-step-animate .ft-step-bullet {\n\t\ttransition-delay: calc(var(--ft-step-arrival, 0) * var(--_beat));\n\t}\n\n\t/* The glyph swap: a short flip on whichever glyph just mounted, and the\n\t * check stroke drawing itself behind it. */\n\t.ft-step-animate .ft-step-glyph {\n\t\ttransform-origin: center;\n\t\tanimation: ft-step-flip 200ms var(--_ease-out) calc(var(--ft-step-arrival, 0) * var(--_beat))\n\t\t\tboth;\n\t}\n\t.ft-step-animate .ft-step-check path {\n\t\tstroke-dasharray: 1;\n\t\tanimation: ft-step-draw 320ms var(--_ease-out)\n\t\t\tcalc(var(--ft-step-arrival, 0) * var(--_beat) + 60ms) both;\n\t}\n\n\t/* The breathing halo — opacity only, no scale — faded in on arrival\n\t * through `filter` so the two animations never fight over `opacity`. */\n\t.ft-step-animate .ft-step-bullet-halo::before {\n\t\tanimation:\n\t\t\tft-step-halo-in 360ms var(--_ease-out) calc(var(--ft-step-arrival, 0) * var(--_beat)) both,\n\t\t\tft-step-breathe 1.2s ease-in-out infinite alternate;\n\t}\n}\n\n@keyframes ft-step-sweep-x {\n\tfrom {\n\t\ttransform: translateX(-100%);\n\t}\n\tto {\n\t\t/* head (right edge, at 45% width) lands on the rail's far end */\n\t\ttransform: translateX(122.3%);\n\t}\n}\n@keyframes ft-step-sweep-y {\n\tfrom {\n\t\ttransform: translateY(-100%);\n\t}\n\tto {\n\t\ttransform: translateY(122.3%);\n\t}\n}\n@keyframes ft-step-sweep-fade {\n\tfrom {\n\t\topacity: 1;\n\t}\n\tto {\n\t\topacity: 0;\n\t}\n}\n@keyframes ft-step-breathe {\n\tfrom {\n\t\topacity: 0.5;\n\t}\n\tto {\n\t\topacity: 1;\n\t}\n}\n@keyframes ft-step-halo-in {\n\tfrom {\n\t\tfilter: opacity(0);\n\t}\n\tto {\n\t\tfilter: opacity(1);\n\t}\n}\n@keyframes ft-step-flip {\n\tfrom {\n\t\ttransform: perspective(60px) rotateX(90deg);\n\t\topacity: 0;\n\t}\n\tto {\n\t\ttransform: perspective(60px) rotateX(0deg);\n\t\topacity: 1;\n\t}\n}\n@keyframes ft-step-draw {\n\tfrom {\n\t\tstroke-dashoffset: 1;\n\t}\n\tto {\n\t\tstroke-dashoffset: 0;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/stepper/step.css"},{"path":"react/src/components/stepper/types.ts","content":"\"use client\";\n\nimport { createContext } from \"react\";\n\n/**\n * The contract between the stepper root and its steps.\n *\n * `Stepper` owns the active index; every `Step` reads it through\n * context instead of a prop threaded down by hand, the same\n * \"root owns the shared state, items read it\" shape a toggle group and\n * its items use for roving focus.\n *\n * A step's own number and status (done/current/upcoming) are derived from\n * its position among its registered siblings, not from any prop the\n * consumer supplies. Registration order is trusted as step order here —\n * unlike the live-DOM-position re-query `menu.ts` does for arrow-key\n * navigation, steps are a static composition (typically one `map` over a\n * fixed list) that doesn't reorder after mount, so the order each `Step`\n * registers in in its own mount effect already *is* DOM order. React runs\n * child effects before parent effects and in mount order, so siblings\n * register top-to-bottom exactly as they do on the Svelte side.\n *\n * That assumption has a consequence, not just a precondition: reordering\n * already-mounted `Step`s (a keyed list that moves an existing item rather\n * than adding or removing one) does not re-run that item's registration\n * effect, so `indexOf` keeps returning its *original* mount position —\n * reordering without add or remove leaves every step's number, status,\n * checkmark and `aria-current` pinned to its original mount position.\n * Adding or removing a step settles correctly, since that's a genuine\n * mount/unmount. Only pure reordering is affected.\n */\nexport interface StepperContext {\n\t/** The rail's stacking axis. */\n\treadonly orientation: \"horizontal\" | \"vertical\";\n\t/** Whether steps render as buttons a reader can click to jump. */\n\treadonly clickable: boolean;\n\t/** The active step's 0-based index. */\n\treadonly current: number;\n\t/** Total number of currently registered steps. */\n\treadonly count: number;\n\t/**\n\t * The active index before the latest change (0 on first paint). Steps\n\t * order their rail sweep and bullet arrival by their distance from it.\n\t * Optional so a hand-rolled context without it still type-checks; a\n\t * missing value reads as \"no run to order\".\n\t */\n\treadonly origin?: number;\n\t/**\n\t * Whether the motion layer (rail sweep, breathing halo, check draw) is\n\t * on: true on the client once `prefers-reduced-motion` has been read and\n\t * is not `reduce`. Optional for the same reason as `origin`.\n\t */\n\treadonly animate?: boolean;\n\t/**\n\t * Registers a step under a stable per-instance id (typically\n\t * `useFancyId()`). Returns an unregister function; call it on unmount.\n\t *\n\t * Identity-stable for the life of the root, on purpose: a `Step`'s\n\t * registration effect depends on this function, and a function rebuilt\n\t * on every context change would re-run that effect — unregister,\n\t * register, re-render, unregister — forever. See `Stepper.tsx`.\n\t */\n\tregister(id: string): () => void;\n\t/** `id`'s 0-based position among registered steps, or -1 before it has registered. */\n\tindexOf(id: string): number;\n\t/** Jumps to `index`, if `clickable`. No-op otherwise. */\n\tselect(index: number): void;\n}\n\n/**\n * The context a `Step` reads to learn its position, its status and the\n * rail's shape. The Svelte source publishes it under a `Symbol` context\n * key; React's own context object plays that role here, so the exported\n * name is kept and the value is a `React.Context` rather than a symbol:\n *\n * ```tsx\n * const stepper = useContext(STEPPER_KEY);\n * ```\n *\n * Read it as optional — a `Step` rendered outside a `Stepper` gets\n * `undefined` rather than throwing, and renders as a plain, always\n * \"upcoming\", never-clickable item.\n */\nexport const STEPPER_KEY = createContext<StepperContext | undefined>(undefined);\nSTEPPER_KEY.displayName = \"StepperContext\";\n","type":"registry:ui","target":"@ui/fancy/components/stepper/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/stepper?framework=react"}}