/** * Result of a per-step validation gate. * `true`/`void` = pass; `false` = generic fail; `string` = fail with that message. * * @example * const validate = (): StepValidationResult => form.isValid || "Fill all fields" */ export type StepValidationResult = boolean | string | void; /** * Per-step config when you want the hook to know optional/validation metadata * without a `Step` tree. * * @example * useStepper({ steps: [{ id: "plan" }, { id: "extras", optional: true }] }) */ export interface StepperStepConfig { /** Stable id for the step (optional; index used if omitted). */ id?: string; /** Marks the step skippable — `next()` succeeds even with no validation pass. Default: `false` */ optional?: boolean; /** Per-step gate. Return false/string to block `next()`. May be async. */ validate?: () => StepValidationResult | Promise; } /** * Options for {@link useStepper}. * * @example * useStepper({ count: 4, linear: true, onComplete: () => router.push("/done") }) */ export interface UseStepperOptions { /** Number of steps. Required when `steps` is omitted. Clamped to ≥ 1. */ count?: number; /** Per-step config. When provided, its length wins over `count`. */ steps?: ReadonlyArray; /** Initial active index when uncontrolled (0-based). Default: `0` */ defaultStep?: number; /** Controlled active index. When provided, the hook is controlled (like useDisclosure). */ activeStep?: number; /** Pre-completed indices when uncontrolled. Default: `[]` */ defaultCompleted?: ReadonlyArray; /** Linear mode: forbid `goTo` jumping forward past an incomplete required step. Default: `true` */ linear?: boolean; /** Global gate run before EVERY `next()` (in addition to the step's own `validate`). */ validate?: (step: number) => StepValidationResult | Promise; /** Fired whenever the active step changes (both modes). */ onStepChange?: (step: number, meta: { direction: 1 | -1; }) => void; /** Fired when the last step is finished via `next()`/`complete()`. */ onComplete?: () => void; } /** * Imperative + derived API returned by {@link useStepper}. * * @example * const s: UseStepperReturn = useStepper({ count: 3 }); * s.canNext; s.next(); s.goTo(0); */ export interface UseStepperReturn { /** Current active index (0-based, clamped to `[0, count-1]`). */ activeStep: number; /** Total number of steps. */ count: number; /** Motion direction of the last transition (1 = forward, -1 = back). */ direction: 1 | -1; /** Immutable set of completed indices. */ completed: ReadonlySet; /** Map of index → error string for steps whose gate failed. */ errors: ReadonlyMap; /** True while an async `validate` is in flight (disable Next button on it). */ isValidating: boolean; /** `true` if there is a next step. */ canNext: boolean; /** `true` if there is a previous step. */ canBack: boolean; /** `true` on the first step. */ isFirst: boolean; /** `true` on the last step. */ isLast: boolean; /** Whether index `i` is configured optional. */ isOptional: (i: number) => boolean; /** Whether index `i` is in the completed set. */ isCompleted: (i: number) => boolean; /** * Advance one step. Runs the step gate then the global gate; if either fails, * stays put and records the error. Marks the current step completed on success. * On the last step, marks complete and fires `onComplete`. Returns a Promise * resolving to whether it advanced/completed. */ next: () => Promise; /** Go back one step (no gate). No-op on first. */ back: () => void; /** * Jump to an arbitrary index. In linear mode, forward jumps past an incomplete * required step are rejected (returns false). Backward jumps always allowed. */ goTo: (step: number) => boolean; /** Mark an index completed without navigating. */ complete: (step?: number) => void; /** Set or clear an error for a step (clear with `null`). */ setStepError: (step: number, message: string | null) => void; /** Reset to `defaultStep`, clear completed + errors. */ reset: () => void; } /** * Headless controlled/uncontrolled wizard state machine. Zero dependencies. * * Controlled detection mirrors `useDisclosure`/`usePagination`: passing * `activeStep` switches the hook into controlled mode, where `next`/`back`/`goTo` * never mutate the active index internally — they only compute the target and * fire `onStepChange`/`onComplete` for the parent to apply. `completed`, * `errors`, and `isValidating` remain hook-internal in both modes (they are * orthogonal to the active index). * * @param {UseStepperOptions} options - Wizard configuration. * @returns {UseStepperReturn} Active index, derived flags, and navigation actions. * * @example * // Uncontrolled, linear, with an async gate on step 1 * const s = useStepper({ * count: 3, * validate: async (i) => i !== 1 || (await check()) || "Try again", * }); * * * @example * // Controlled: parent owns the index * const s = useStepper({ activeStep: step, onStepChange: (i) => setStep(i) }); */ export declare function useStepper(options: UseStepperOptions): UseStepperReturn;