{ "name": "stepper", "type": "registry:ui", "dependencies": [ "radix-ui" ], "registryDependencies": [ "button", "@diceui/use-as-ref", "@diceui/use-isomorphic-layout-effect", "@diceui/use-lazy-ref" ], "files": [ { "path": "ui/stepper.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport {\n Direction as DirectionPrimitive,\n Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"Stepper\";\nconst LIST_NAME = \"StepperList\";\nconst ITEM_NAME = \"StepperItem\";\nconst TRIGGER_NAME = \"StepperTrigger\";\nconst INDICATOR_NAME = \"StepperIndicator\";\nconst SEPARATOR_NAME = \"StepperSeparator\";\nconst TITLE_NAME = \"StepperTitle\";\nconst DESCRIPTION_NAME = \"StepperDescription\";\nconst CONTENT_NAME = \"StepperContent\";\nconst PREV_NAME = \"StepperPrev\";\nconst NEXT_NAME = \"StepperNext\";\n\nconst ENTRY_FOCUS = \"stepperFocusGroup.onEntryFocus\";\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\nconst ARROW_KEYS = [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"];\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\ntype NavigationDirection = \"next\" | \"prev\";\ntype ActivationMode = \"automatic\" | \"manual\";\ntype DataState = \"inactive\" | \"active\" | \"completed\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\ninterface ButtonProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\ntype ListElement = React.ComponentRef;\ntype TriggerElement = React.ComponentRef;\n\nfunction getId(\n id: string,\n variant: \"trigger\" | \"content\" | \"title\" | \"description\",\n value: string,\n) {\n return `${id}-${variant}-${value}`;\n}\n\ntype FocusIntent = \"first\" | \"last\" | \"prev\" | \"next\";\n\nconst MAP_KEY_TO_FOCUS_INTENT: Record = {\n ArrowLeft: \"prev\",\n ArrowUp: \"prev\",\n ArrowRight: \"next\",\n ArrowDown: \"next\",\n PageUp: \"first\",\n Home: \"first\",\n PageDown: \"last\",\n End: \"last\",\n};\n\nfunction getDirectionAwareKey(key: string, dir?: Direction) {\n if (dir !== \"rtl\") return key;\n return key === \"ArrowLeft\"\n ? \"ArrowRight\"\n : key === \"ArrowRight\"\n ? \"ArrowLeft\"\n : key;\n}\n\nfunction getFocusIntent(\n event: React.KeyboardEvent,\n dir?: Direction,\n orientation?: Orientation,\n) {\n const key = getDirectionAwareKey(event.key, dir);\n if (orientation === \"horizontal\" && [\"ArrowUp\", \"ArrowDown\"].includes(key))\n return undefined;\n if (orientation === \"vertical\" && [\"ArrowLeft\", \"ArrowRight\"].includes(key))\n return undefined;\n return MAP_KEY_TO_FOCUS_INTENT[key];\n}\n\nfunction focusFirst(\n candidates: React.RefObject[],\n preventScroll = false,\n) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidateRef of candidates) {\n const candidate = candidateRef.current;\n if (!candidate) continue;\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus({ preventScroll });\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\nfunction wrapArray(array: T[], startIndex: number) {\n return array.map(\n (_, index) => array[(startIndex + index) % array.length] as T,\n );\n}\n\nfunction getDataState(\n value: string | undefined,\n itemValue: string,\n stepState: StepState | undefined,\n steps: Map,\n variant: \"item\" | \"separator\" = \"item\",\n): DataState {\n const stepKeys = Array.from(steps.keys());\n const currentIndex = stepKeys.indexOf(itemValue);\n\n if (stepState?.completed) return \"completed\";\n\n if (value === itemValue) {\n return variant === \"separator\" ? \"inactive\" : \"active\";\n }\n\n if (value) {\n const activeIndex = stepKeys.indexOf(value);\n\n if (activeIndex > currentIndex) return \"completed\";\n }\n\n return \"inactive\";\n}\n\ninterface StepState {\n value: string;\n completed: boolean;\n disabled: boolean;\n}\n\ninterface StoreState {\n steps: Map;\n value: string;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n setState: (key: K, value: StoreState[K]) => void;\n setStateWithValidation: (\n value: string,\n direction: NavigationDirection,\n ) => Promise;\n hasValidation: () => boolean;\n notify: () => void;\n addStep: (value: string, completed: boolean, disabled: boolean) => void;\n removeStep: (value: string) => void;\n setStep: (value: string, completed: boolean, disabled: boolean) => void;\n}\n\nconst StoreContext = React.createContext(null);\n\nfunction useStoreContext(consumerName: string) {\n const context = React.useContext(StoreContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\nfunction useStore(selector: (state: StoreState) => T): T {\n const store = useStoreContext(\"useStore\");\n\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ItemData {\n id: string;\n ref: React.RefObject;\n value: string;\n active: boolean;\n disabled: boolean;\n}\n\ninterface StepperContextValue {\n rootId: string;\n dir: Direction;\n orientation: Orientation;\n activationMode: ActivationMode;\n disabled: boolean;\n nonInteractive: boolean;\n loop: boolean;\n}\n\nconst StepperContext = React.createContext(null);\n\nfunction useStepperContext(consumerName: string) {\n const context = React.useContext(StepperContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface StepperProps extends DivProps {\n value?: string;\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n onValueComplete?: (value: string, completed: boolean) => void;\n onValueAdd?: (value: string) => void;\n onValueRemove?: (value: string) => void;\n onValidate?: (\n value: string,\n direction: NavigationDirection,\n ) => boolean | Promise;\n activationMode?: ActivationMode;\n dir?: Direction;\n orientation?: Orientation;\n disabled?: boolean;\n loop?: boolean;\n nonInteractive?: boolean;\n}\n\nfunction Stepper(props: StepperProps) {\n const {\n value,\n defaultValue,\n onValueChange,\n onValueComplete,\n onValueAdd,\n onValueRemove,\n onValidate,\n dir: dirProp,\n orientation = \"horizontal\",\n activationMode = \"automatic\",\n asChild,\n disabled = false,\n nonInteractive = false,\n loop = false,\n className,\n id,\n ...rootProps\n } = props;\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n steps: new Map(),\n value: value ?? defaultValue ?? \"\",\n }));\n\n const propsRef = useAsRef({\n onValueChange,\n onValueComplete,\n onValueAdd,\n onValueRemove,\n onValidate,\n });\n\n const store = React.useMemo(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n\n if (key === \"value\" && typeof value === \"string\") {\n stateRef.current.value = value;\n propsRef.current.onValueChange?.(value);\n } else {\n stateRef.current[key] = value;\n }\n\n store.notify();\n },\n setStateWithValidation: async (value, direction) => {\n if (!propsRef.current.onValidate) {\n store.setState(\"value\", value);\n return true;\n }\n\n try {\n const isValid = await propsRef.current.onValidate(value, direction);\n if (isValid) {\n store.setState(\"value\", value);\n }\n return isValid;\n } catch {\n return false;\n }\n },\n hasValidation: () => !!propsRef.current.onValidate,\n addStep: (value, completed, disabled) => {\n const newStep: StepState = { value, completed, disabled };\n stateRef.current.steps.set(value, newStep);\n propsRef.current.onValueAdd?.(value);\n store.notify();\n },\n removeStep: (value) => {\n stateRef.current.steps.delete(value);\n propsRef.current.onValueRemove?.(value);\n store.notify();\n },\n setStep: (value, completed, disabled) => {\n const step = stateRef.current.steps.get(value);\n if (step) {\n const updatedStep: StepState = { ...step, completed, disabled };\n stateRef.current.steps.set(value, updatedStep);\n\n if (completed !== step.completed) {\n propsRef.current.onValueComplete?.(value, completed);\n }\n\n store.notify();\n }\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef, propsRef]);\n\n useIsomorphicLayoutEffect(() => {\n if (value !== undefined) {\n store.setState(\"value\", value);\n }\n }, [value]);\n\n const dir = DirectionPrimitive.useDirection(dirProp);\n\n const instanceId = React.useId();\n const rootId = id ?? instanceId;\n\n const contextValue = React.useMemo(\n () => ({\n rootId,\n dir,\n orientation,\n activationMode,\n disabled,\n nonInteractive,\n loop,\n }),\n [rootId, dir, orientation, activationMode, disabled, nonInteractive, loop],\n );\n\n const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n \n \n \n \n );\n}\n\ninterface FocusContextValue {\n tabStopId: string | null;\n onItemFocus: (tabStopId: string) => void;\n onItemShiftTab: () => void;\n onFocusableItemAdd: () => void;\n onFocusableItemRemove: () => void;\n onItemRegister: (item: ItemData) => void;\n onItemUnregister: (id: string) => void;\n getItems: () => ItemData[];\n}\n\nconst FocusContext = React.createContext(null);\n\nfunction useFocusContext(consumerName: string) {\n const context = React.useContext(FocusContext);\n if (!context) {\n throw new Error(\n `\\`${consumerName}\\` must be used within \\`FocusProvider\\``,\n );\n }\n return context;\n}\n\nfunction StepperList(props: DivProps) {\n const {\n asChild,\n onBlur: onBlurProp,\n onFocus: onFocusProp,\n onMouseDown: onMouseDownProp,\n className,\n children,\n ref,\n ...listProps\n } = props;\n\n const context = useStepperContext(LIST_NAME);\n const orientation = context.orientation;\n const currentValue = useStore((state) => state.value);\n\n const propsRef = useAsRef({\n onBlur: onBlurProp,\n onFocus: onFocusProp,\n onMouseDown: onMouseDownProp,\n });\n\n const [tabStopId, setTabStopId] = React.useState(null);\n const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false);\n const [focusableItemCount, setFocusableItemCount] = React.useState(0);\n const isClickFocusRef = React.useRef(false);\n const itemsRef = React.useRef>(new Map());\n const listRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, listRef);\n\n const onItemFocus = React.useCallback((tabStopId: string) => {\n setTabStopId(tabStopId);\n }, []);\n\n const onItemShiftTab = React.useCallback(() => {\n setIsTabbingBackOut(true);\n }, []);\n\n const onFocusableItemAdd = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount + 1);\n }, []);\n\n const onFocusableItemRemove = React.useCallback(() => {\n setFocusableItemCount((prevCount) => prevCount - 1);\n }, []);\n\n const onItemRegister = React.useCallback((item: ItemData) => {\n itemsRef.current.set(item.id, item);\n }, []);\n\n const onItemUnregister = React.useCallback((id: string) => {\n itemsRef.current.delete(id);\n }, []);\n\n const getItems = React.useCallback(() => {\n return Array.from(itemsRef.current.values())\n .filter((item) => item.ref.current)\n .sort((a, b) => {\n const elementA = a.ref.current;\n const elementB = b.ref.current;\n if (!elementA || !elementB) return 0;\n const position = elementA.compareDocumentPosition(elementB);\n if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n return -1;\n }\n if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n return 1;\n }\n return 0;\n });\n }, []);\n\n const onBlur = React.useCallback(\n (event: React.FocusEvent) => {\n propsRef.current.onBlur?.(event);\n if (event.defaultPrevented) return;\n\n setIsTabbingBackOut(false);\n },\n [propsRef],\n );\n\n const onFocus = React.useCallback(\n (event: React.FocusEvent) => {\n propsRef.current.onFocus?.(event);\n if (event.defaultPrevented) return;\n\n const isKeyboardFocus = !isClickFocusRef.current;\n if (\n event.target === event.currentTarget &&\n isKeyboardFocus &&\n !isTabbingBackOut\n ) {\n const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);\n event.currentTarget.dispatchEvent(entryFocusEvent);\n\n if (!entryFocusEvent.defaultPrevented) {\n const items = Array.from(itemsRef.current.values()).filter(\n (item) => !item.disabled,\n );\n const selectedItem = currentValue\n ? items.find((item) => item.value === currentValue)\n : undefined;\n const activeItem = items.find((item) => item.active);\n const currentItem = items.find((item) => item.id === tabStopId);\n\n const candidateItems = [\n selectedItem,\n activeItem,\n currentItem,\n ...items,\n ].filter(Boolean) as ItemData[];\n const candidateRefs = candidateItems.map((item) => item.ref);\n focusFirst(candidateRefs, false);\n }\n }\n isClickFocusRef.current = false;\n },\n [propsRef, isTabbingBackOut, currentValue, tabStopId],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseDown?.(event);\n\n if (event.defaultPrevented) return;\n\n isClickFocusRef.current = true;\n },\n [propsRef],\n );\n\n const focusContextValue = React.useMemo(\n () => ({\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems,\n }),\n [\n tabStopId,\n onItemFocus,\n onItemShiftTab,\n onFocusableItemAdd,\n onFocusableItemRemove,\n onItemRegister,\n onItemUnregister,\n getItems,\n ],\n );\n\n const ListPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface StepperItemContextValue {\n value: string;\n stepState: StepState | undefined;\n}\n\nconst StepperItemContext = React.createContext(\n null,\n);\n\nfunction useStepperItemContext(consumerName: string) {\n const context = React.useContext(StepperItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\ninterface StepperItemProps extends DivProps {\n value: string;\n completed?: boolean;\n disabled?: boolean;\n}\n\nfunction StepperItem(props: StepperItemProps) {\n const {\n value: itemValue,\n completed = false,\n disabled = false,\n asChild,\n className,\n children,\n ref,\n ...itemProps\n } = props;\n\n const context = useStepperContext(ITEM_NAME);\n const store = useStoreContext(ITEM_NAME);\n const orientation = context.orientation;\n const value = useStore((state) => state.value);\n\n useIsomorphicLayoutEffect(() => {\n store.addStep(itemValue, completed, disabled);\n\n return () => {\n store.removeStep(itemValue);\n };\n }, [itemValue, completed, disabled]);\n\n useIsomorphicLayoutEffect(() => {\n store.setStep(itemValue, completed, disabled);\n }, [itemValue, completed, disabled]);\n\n const stepState = useStore((state) => state.steps.get(itemValue));\n const steps = useStore((state) => state.steps);\n const dataState = getDataState(value, itemValue, stepState, steps);\n\n const itemContextValue = React.useMemo(\n () => ({\n value: itemValue,\n stepState,\n }),\n [itemValue, stepState],\n );\n\n const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n \n {children}\n \n \n );\n}\n\nfunction StepperTrigger(props: ButtonProps) {\n const {\n asChild,\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n onMouseDown: onMouseDownProp,\n disabled,\n className,\n ref,\n ...triggerProps\n } = props;\n\n const context = useStepperContext(TRIGGER_NAME);\n const itemContext = useStepperItemContext(TRIGGER_NAME);\n const itemValue = itemContext.value;\n\n const store = useStoreContext(TRIGGER_NAME);\n const focusContext = useFocusContext(TRIGGER_NAME);\n const value = useStore((state) => state.value);\n const steps = useStore((state) => state.steps);\n const stepState = useStore((state) => state.steps.get(itemValue));\n\n const propsRef = useAsRef({\n onClick: onClickProp,\n onFocus: onFocusProp,\n onKeyDown: onKeyDownProp,\n onMouseDown: onMouseDownProp,\n });\n\n const activationMode = context.activationMode;\n const orientation = context.orientation;\n const loop = context.loop;\n\n const stepIndex = Array.from(steps.keys()).indexOf(itemValue);\n\n const stepPosition = stepIndex + 1;\n const stepCount = steps.size;\n\n const triggerId = getId(context.rootId, \"trigger\", itemValue);\n const contentId = getId(context.rootId, \"content\", itemValue);\n const titleId = getId(context.rootId, \"title\", itemValue);\n const descriptionId = getId(context.rootId, \"description\", itemValue);\n\n const isDisabled = disabled || stepState?.disabled || context.disabled;\n const isActive = value === itemValue;\n const isTabStop = focusContext.tabStopId === triggerId;\n const dataState = getDataState(value, itemValue, stepState, steps);\n\n const triggerRef = React.useRef(null);\n const composedRef = useComposedRefs(ref, triggerRef);\n const isArrowKeyPressedRef = React.useRef(false);\n const isMouseClickRef = React.useRef(false);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (ARROW_KEYS.includes(event.key)) {\n isArrowKeyPressedRef.current = true;\n }\n }\n function onKeyUp() {\n isArrowKeyPressedRef.current = false;\n }\n document.addEventListener(\"keydown\", onKeyDown);\n document.addEventListener(\"keyup\", onKeyUp);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n document.removeEventListener(\"keyup\", onKeyUp);\n };\n }, []);\n\n useIsomorphicLayoutEffect(() => {\n focusContext.onItemRegister({\n id: triggerId,\n ref: triggerRef,\n value: itemValue,\n active: isTabStop,\n disabled: !!isDisabled,\n });\n\n if (!isDisabled) {\n focusContext.onFocusableItemAdd();\n }\n\n return () => {\n focusContext.onItemUnregister(triggerId);\n if (!isDisabled) {\n focusContext.onFocusableItemRemove();\n }\n };\n }, [focusContext, triggerId, itemValue, isTabStop, isDisabled]);\n\n const onClick = React.useCallback(\n async (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n if (event.defaultPrevented) return;\n\n if (!isDisabled && !context.nonInteractive) {\n const currentStepIndex = Array.from(steps.keys()).indexOf(value ?? \"\");\n const targetStepIndex = Array.from(steps.keys()).indexOf(itemValue);\n const direction = targetStepIndex > currentStepIndex ? \"next\" : \"prev\";\n\n await store.setStateWithValidation(itemValue, direction);\n }\n },\n [\n isDisabled,\n context.nonInteractive,\n store,\n itemValue,\n value,\n steps,\n propsRef,\n ],\n );\n\n const onFocus = React.useCallback(\n async (event: React.FocusEvent) => {\n propsRef.current.onFocus?.(event);\n if (event.defaultPrevented) return;\n\n focusContext.onItemFocus(triggerId);\n\n const isKeyboardFocus = !isMouseClickRef.current;\n\n if (\n !isActive &&\n !isDisabled &&\n activationMode !== \"manual\" &&\n !context.nonInteractive &&\n isKeyboardFocus\n ) {\n const currentStepIndex = Array.from(steps.keys()).indexOf(value || \"\");\n const targetStepIndex = Array.from(steps.keys()).indexOf(itemValue);\n const direction = targetStepIndex > currentStepIndex ? \"next\" : \"prev\";\n\n await store.setStateWithValidation(itemValue, direction);\n }\n\n isMouseClickRef.current = false;\n },\n [\n focusContext,\n triggerId,\n activationMode,\n isActive,\n isDisabled,\n context.nonInteractive,\n store,\n itemValue,\n value,\n steps,\n propsRef,\n ],\n );\n\n const onKeyDown = React.useCallback(\n async (event: React.KeyboardEvent) => {\n propsRef.current.onKeyDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.key === \"Enter\" && context.nonInteractive) {\n event.preventDefault();\n return;\n }\n\n if (\n (event.key === \"Enter\" || event.key === \" \") &&\n activationMode === \"manual\" &&\n !context.nonInteractive\n ) {\n event.preventDefault();\n if (!isDisabled && triggerRef.current) {\n triggerRef.current.click();\n }\n return;\n }\n\n if (event.key === \"Tab\" && event.shiftKey) {\n focusContext.onItemShiftTab();\n return;\n }\n\n if (event.target !== event.currentTarget) return;\n\n const focusIntent = getFocusIntent(event, context.dir, orientation);\n\n if (focusIntent !== undefined) {\n if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)\n return;\n event.preventDefault();\n\n const items = focusContext.getItems().filter((item) => !item.disabled);\n let candidateRefs = items.map((item) => item.ref);\n\n if (focusIntent === \"last\") {\n candidateRefs.reverse();\n } else if (focusIntent === \"prev\" || focusIntent === \"next\") {\n if (focusIntent === \"prev\") candidateRefs.reverse();\n const currentIndex = candidateRefs.findIndex(\n (ref) => ref.current === event.currentTarget,\n );\n candidateRefs = loop\n ? wrapArray(candidateRefs, currentIndex + 1)\n : candidateRefs.slice(currentIndex + 1);\n }\n\n if (store.hasValidation() && candidateRefs.length > 0) {\n const nextRef = candidateRefs[0];\n const nextElement = nextRef?.current;\n const nextItem = items.find(\n (item) => item.ref.current === nextElement,\n );\n\n if (nextItem && nextItem.value !== itemValue) {\n const currentStepIndex = Array.from(steps.keys()).indexOf(\n value || \"\",\n );\n const targetStepIndex = Array.from(steps.keys()).indexOf(\n nextItem.value,\n );\n const direction: NavigationDirection =\n targetStepIndex > currentStepIndex ? \"next\" : \"prev\";\n\n if (direction === \"next\") {\n const isValid = await store.setStateWithValidation(\n nextItem.value,\n direction,\n );\n if (!isValid) return;\n } else {\n store.setState(\"value\", nextItem.value);\n }\n\n queueMicrotask(() => nextElement?.focus());\n return;\n }\n }\n\n queueMicrotask(() => focusFirst(candidateRefs));\n }\n },\n [\n focusContext,\n context.nonInteractive,\n context.dir,\n activationMode,\n orientation,\n loop,\n isDisabled,\n store,\n propsRef,\n itemValue,\n value,\n steps,\n ],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n propsRef.current.onMouseDown?.(event);\n if (event.defaultPrevented) return;\n\n isMouseClickRef.current = true;\n\n if (isDisabled) {\n event.preventDefault();\n } else {\n focusContext.onItemFocus(triggerId);\n }\n },\n [focusContext, triggerId, isDisabled, propsRef],\n );\n\n const TriggerPrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n return (\n \n );\n}\n\ninterface StepperIndicatorProps extends Omit {\n children?: React.ReactNode | ((dataState: DataState) => React.ReactNode);\n}\n\nfunction StepperIndicator(props: StepperIndicatorProps) {\n const { className, children, asChild, ref, ...indicatorProps } = props;\n\n const context = useStepperContext(INDICATOR_NAME);\n const itemContext = useStepperItemContext(INDICATOR_NAME);\n\n const value = useStore((state) => state.value);\n const itemValue = itemContext.value;\n const stepState = useStore((state) => state.steps.get(itemValue));\n const steps = useStore((state) => state.steps);\n\n const stepPosition = Array.from(steps.keys()).indexOf(itemValue) + 1;\n\n const dataState = getDataState(value, itemValue, stepState, steps);\n\n const IndicatorPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n {typeof children === \"function\" ? (\n children(dataState)\n ) : children ? (\n children\n ) : dataState === \"completed\" ? (\n \n ) : (\n stepPosition\n )}\n \n );\n}\n\ninterface StepperSeparatorProps extends DivProps {\n forceMount?: boolean;\n}\n\nfunction StepperSeparator(props: StepperSeparatorProps) {\n const {\n className,\n asChild,\n forceMount = false,\n ref,\n ...separatorProps\n } = props;\n\n const context = useStepperContext(SEPARATOR_NAME);\n const itemContext = useStepperItemContext(SEPARATOR_NAME);\n const value = useStore((state) => state.value);\n const steps = useStore((state) => state.steps);\n\n const orientation = context.orientation;\n\n const stepIndex = Array.from(steps.keys()).indexOf(itemContext.value);\n\n const isLastStep = stepIndex === steps.size - 1;\n\n if (isLastStep && !forceMount) return null;\n\n const dataState = getDataState(\n value,\n itemContext.value,\n itemContext.stepState,\n steps,\n \"separator\",\n );\n\n const SeparatorPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n );\n}\n\ninterface StepperTitleProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction StepperTitle(props: StepperTitleProps) {\n const { className, asChild, ref, ...titleProps } = props;\n\n const context = useStepperContext(TITLE_NAME);\n const itemContext = useStepperItemContext(TITLE_NAME);\n\n const titleId = getId(context.rootId, \"title\", itemContext.value);\n\n const TitlePrimitive = asChild ? SlotPrimitive.Slot : \"span\";\n\n return (\n \n );\n}\n\ninterface StepperDescriptionProps extends React.ComponentProps<\"span\"> {\n asChild?: boolean;\n}\n\nfunction StepperDescription(props: StepperDescriptionProps) {\n const { className, asChild, ref, ...descriptionProps } = props;\n\n const context = useStepperContext(DESCRIPTION_NAME);\n const itemContext = useStepperItemContext(DESCRIPTION_NAME);\n\n const descriptionId = getId(context.rootId, \"description\", itemContext.value);\n\n const DescriptionPrimitive = asChild ? SlotPrimitive.Slot : \"span\";\n\n return (\n \n );\n}\n\ninterface StepperContentProps extends DivProps {\n value: string;\n forceMount?: boolean;\n}\n\nfunction StepperContent(props: StepperContentProps) {\n const {\n value: valueProp,\n asChild,\n forceMount = false,\n ref,\n className,\n ...contentProps\n } = props;\n\n const context = useStepperContext(CONTENT_NAME);\n const value = useStore((state) => state.value);\n\n const contentId = getId(context.rootId, \"content\", valueProp);\n const triggerId = getId(context.rootId, \"trigger\", valueProp);\n\n if (valueProp !== value && !forceMount) return null;\n\n const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n );\n}\n\nfunction StepperPrev(props: ButtonProps) {\n const { asChild, onClick: onClickProp, disabled, ...prevProps } = props;\n\n const store = useStoreContext(PREV_NAME);\n const value = useStore((state) => state.value);\n const steps = useStore((state) => state.steps);\n\n const propsRef = useAsRef({\n onClick: onClickProp,\n });\n\n const stepKeys = Array.from(steps.keys());\n const currentIndex = value ? stepKeys.indexOf(value) : -1;\n const isDisabled = disabled || currentIndex <= 0;\n\n const onClick = React.useCallback(\n async (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n if (event.defaultPrevented || isDisabled) return;\n\n const prevIndex = Math.max(currentIndex - 1, 0);\n const prevStepValue = stepKeys[prevIndex];\n\n if (prevStepValue) {\n store.setState(\"value\", prevStepValue);\n }\n },\n [propsRef, isDisabled, currentIndex, stepKeys, store],\n );\n\n const PrevPrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n return (\n \n );\n}\n\nfunction StepperNext(props: ButtonProps) {\n const { asChild, onClick: onClickProp, disabled, ...nextProps } = props;\n\n const store = useStoreContext(NEXT_NAME);\n const value = useStore((state) => state.value);\n const steps = useStore((state) => state.steps);\n\n const propsRef = useAsRef({\n onClick: onClickProp,\n });\n\n const stepKeys = Array.from(steps.keys());\n const currentIndex = value ? stepKeys.indexOf(value) : -1;\n const isDisabled = disabled || currentIndex >= stepKeys.length - 1;\n\n const onClick = React.useCallback(\n async (event: React.MouseEvent) => {\n propsRef.current.onClick?.(event);\n if (event.defaultPrevented || isDisabled) return;\n\n const nextIndex = Math.min(currentIndex + 1, stepKeys.length - 1);\n const nextStepValue = stepKeys[nextIndex];\n\n if (nextStepValue) {\n await store.setStateWithValidation(nextStepValue, \"next\");\n }\n },\n [propsRef, isDisabled, currentIndex, stepKeys, store],\n );\n\n const NextPrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n return (\n \n );\n}\n\nexport {\n Stepper,\n StepperContent,\n StepperDescription,\n StepperIndicator,\n StepperItem,\n StepperList,\n StepperNext,\n StepperPrev,\n type StepperProps,\n StepperSeparator,\n StepperTitle,\n StepperTrigger,\n useStore as useStepper,\n};\n", "target": "" }, { "path": "lib/compose-refs.ts", "type": "registry:lib", "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ] }