{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-multi-step-form",
  "type": "registry:component",
  "title": "MultiStepForm",
  "description": "A gated wizard over Stepper, submitted on the last step",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/internal/button.tsx",
      "target": "@ui/gpuixui/components/internal/button.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport type { ReactNode } from \"react\";\nimport {\n  Button as PrimitiveButton,\n  IconButton as PrimitiveIconButton,\n  actionOnColor as primitiveActionOnColor,\n  type ButtonVariant as PrimitiveVariant,\n} from \"../../primitives/button.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Schema from \"effect/Schema\";\nimport type { Theme } from \"./theme.js\";\n\nconst ButtonVariants = Schema.Literals([\n  \"primary\",\n  \"secondary\",\n  \"ghost\",\n  \"danger\",\n  \"link\",\n]);\n\nexport type ButtonVariant = typeof ButtonVariants.Type;\n\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface ButtonProps extends Props {\n  readonly variant?: ButtonVariant;\n\n  readonly size?: ButtonSize;\n\n  readonly disabled?: boolean;\n\n  readonly loading?: boolean;\n\n  readonly loadingLabel?: string;\n\n  readonly reducedMotion?: boolean;\n\n  readonly leading?: ReactNode;\n\n  readonly trailing?: ReactNode;\n}\n\nexport interface IconButtonProps extends Props {\n  readonly label: string;\n\n  readonly size?: ButtonSize;\n  readonly disabled?: boolean;\n}\n\ninterface Look {\n  readonly variant: PrimitiveVariant;\n  readonly className: string;\n}\n\nconst LOOKS: Readonly<Record<ButtonVariant, Look>> = {\n  primary: { variant: \"primary\", className: \"\" },\n  secondary: { variant: \"secondary\", className: \"border border-strong\" },\n  ghost: { variant: \"ghost\", className: \"\" },\n  danger: { variant: \"destructive\", className: \"\" },\n\n  link: { variant: \"link\", className: \"underline underline-offset-4\" },\n};\n\nconst lookOf = (variant: unknown): Look =>\n  LOOKS[Schema.is(ButtonVariants)(variant) ? variant : \"secondary\"];\n\nexport const actionOnColor = (theme: Theme, variant: ButtonVariant): string =>\n  primitiveActionOnColor(theme, lookOf(variant).variant);\n\nexport const Button = ({\n  variant = \"secondary\",\n  size = \"md\",\n  disabled = false,\n  onClick,\n  style,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: ButtonProps) => {\n  const look = lookOf(variant);\n  return (\n    <PrimitiveButton\n      {...rest}\n      variant={look.variant}\n      size={size}\n      disabled={disabled}\n      label={ariaLabel}\n      className={cn(look.className)}\n      onActivate={onClick}\n      style={style}\n    />\n  );\n};\n\nexport const IconButton = ({\n  label,\n  size = \"md\",\n  disabled = false,\n  onClick,\n  style,\n  children,\n  ...rest\n}: IconButtonProps) => (\n  <PrimitiveIconButton\n    {...rest}\n    icon={children}\n    variant=\"ghost\"\n    size={size}\n    disabled={disabled}\n    label={label}\n    className=\"text-muted-foreground hover:text-foreground\"\n    onActivate={onClick}\n    style={style}\n  />\n);\n"
    },
    {
      "path": "packages/components/src/internal/controllable.ts",
      "target": "@ui/gpuixui/components/internal/controllable.ts",
      "type": "registry:lib",
      "content": "import { useCallback, useState } from \"react\";\n\nexport type ControllableState<Value> = readonly [Value, (next: Value) => void];\n\nexport interface ControllableStateOptions<Value> {\n  readonly value: Value | undefined;\n\n  readonly defaultValue: Value;\n\n  readonly onChange?: (next: Value) => void;\n}\n\nexport const useControllableState = <Value>({\n  value,\n  defaultValue,\n  onChange,\n}: ControllableStateOptions<Value>): ControllableState<Value> => {\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n\n  const setValue = useCallback(\n    (next: Value) => {\n      if (!controlled) setInternal(next);\n      if (value !== next) onChange?.(next);\n    },\n    [controlled, value, onChange],\n  );\n\n  return [controlled ? value : internal, setValue];\n};\n"
    },
    {
      "path": "packages/components/src/internal/parts.ts",
      "target": "@ui/gpuixui/components/internal/parts.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\nexport type PartComponent = (props: never) => ReactNode;\n\nexport type PartChildren =\n  | ReactElement<unknown>\n  | boolean\n  | null\n  | undefined\n  | ReadonlyArray<PartChildren>;\n\nexport interface PartsContract {\n  readonly parent: string;\n\n  readonly family: ReadonlyArray<PartComponent>;\n\n  readonly accepts: ReadonlyArray<PartComponent>;\n\n  readonly hint?: string;\n}\n\nconst componentName = (type: unknown): string =>\n  P.isString(type)\n    ? type\n    : P.isFunction(type)\n      ? type.name || \"anonymous\"\n      : \"element\";\n\nconst fragmentChildren = (element: ReactElement<unknown>): ReactNode =>\n  hasChildren(element.props) ? element.props.children : undefined;\n\nconst hasChildren = (\n  value: unknown,\n): value is { readonly children?: ReactNode } =>\n  P.isObject(value) && \"children\" in value;\n\nconst leaves = (children: ReactNode): ReadonlyArray<unknown> =>\n  Arr.flatMap(Children.toArray(children), (child) =>\n    isValidElement<unknown>(child) && child.type === Fragment\n      ? leaves(fragmentChildren(child))\n      : [child],\n  );\n\nexport const assertParts = (\n  contract: PartsContract,\n  children: ReactNode,\n): void => {\n  Arr.forEach(leaves(children), (leaf) => {\n    if (!isValidElement(leaf)) return;\n    if (Arr.some(contract.accepts, (part) => part === leaf.type)) return;\n    if (!Arr.some(contract.family, (part) => part === leaf.type)) return;\n\n    throw new Error(\n      `${componentName(leaf.type)} cannot be used inside ${contract.parent}.` +\n        (contract.hint === undefined ? \"\" : ` ${contract.hint}`),\n    );\n  });\n};\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/text.ts",
      "target": "@ui/gpuixui/components/internal/text.ts",
      "type": "registry:lib",
      "content": "import { pipe } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport * as Str from \"effect/String\";\n\nexport const textOf = (value: unknown): string =>\n  typeof value === \"string\" ? value : \"\";\n\nexport const nameOf = (value: unknown): string | undefined =>\n  pipe(\n    Option.some(textOf(value)),\n    Option.map(Str.trim),\n    Option.filter(Str.isNonEmpty),\n    Option.getOrUndefined,\n  );\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    },
    {
      "path": "packages/components/src/multi-step-form.tsx",
      "target": "@ui/gpuixui/components/multi-step-form.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { accessibleName } from \"../primitives/core/index.js\";\nimport { cn } from \"../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport { useState, type ReactNode } from \"react\";\nimport { Button } from \"./internal/button.js\";\nimport { useControllableState } from \"./internal/controllable.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { textOf } from \"./internal/text.js\";\nimport { statusTone, useTheme } from \"./internal/theme.js\";\nimport { Stepper, StepperStep } from \"./stepper.js\";\nimport { stepName } from \"./stepper/logic.js\";\nimport {\n  gateMessage,\n  isLastStep,\n  nextStep,\n  previousStep,\n  refusalText,\n  stepAnnouncement,\n  stepOf,\n  stepsOf,\n  type StepGate,\n} from \"./wizard/logic.js\";\n\nexport interface MultiStep extends StepGate {\n  readonly id: string;\n\n  readonly title: string;\n\n  readonly description?: string;\n\n  readonly content: ReactNode;\n}\n\nexport interface MultiStepFormProps extends Omit<\n  Props,\n  \"children\" | \"onSubmit\"\n> {\n  readonly steps: ReadonlyArray<MultiStep>;\n\n  readonly step?: number;\n\n  readonly defaultStep?: number;\n\n  readonly onStepChange?: (step: number) => void;\n\n  readonly onSubmit: () => void;\n\n  readonly pending?: boolean;\n\n  readonly submitLabel?: string;\n\n  readonly nextLabel?: string;\n\n  readonly backLabel?: string;\n\n  readonly label?: string;\n  readonly className?: string;\n}\n\nconst DEFAULT_SUBMIT_LABEL = \"Submit\";\nconst DEFAULT_NEXT_LABEL = \"Next\";\nconst DEFAULT_BACK_LABEL = \"Back\";\n\ninterface Refusal {\n  readonly id: string;\n  readonly message: string;\n}\n\nconst guard = (step: MultiStep | undefined): Option.Option<string> =>\n  gateMessage(step);\n\nexport const MultiStepForm = ({\n  steps,\n  step: stepProp,\n  defaultStep = 1,\n  onStepChange,\n  onSubmit,\n  pending = false,\n  submitLabel = DEFAULT_SUBMIT_LABEL,\n  nextLabel = DEFAULT_NEXT_LABEL,\n  backLabel = DEFAULT_BACK_LABEL,\n  label,\n  className,\n  style,\n  ...rest\n}: MultiStepFormProps) => {\n  const theme = useTheme();\n  const name = accessibleName(textOf(label));\n\n  const submitWord =\n    accessibleName(textOf(submitLabel)) ?? DEFAULT_SUBMIT_LABEL;\n  const nextWord = accessibleName(textOf(nextLabel)) ?? DEFAULT_NEXT_LABEL;\n  const backWord = accessibleName(textOf(backLabel)) ?? DEFAULT_BACK_LABEL;\n\n  const list = stepsOf(steps);\n  const count = list.length;\n  const [value, setStep] = useControllableState({\n    value: stepProp,\n    defaultValue: defaultStep,\n    onChange: onStepChange,\n  });\n  const current = stepOf(value, count);\n  const [refusal, setRefusal] = useState<Option.Option<Refusal>>(Option.none());\n\n  if (count === 0) return null;\n\n  const step = list[current - 1];\n  if (step === undefined) return null;\n  const title = stepName(step.title, current);\n  const last = isLastStep(current, count);\n  const message = Option.flatMap(refusal, (held) =>\n    held.id === step.id ? Option.some(held.message) : Option.none(),\n  );\n\n  const advance = (): void => {\n    Option.match(guard(step), {\n      onNone: () => {\n        setRefusal(Option.none());\n        setStep(nextStep(current, count));\n      },\n      onSome: (next) =>\n        setRefusal(Option.some({ id: step.id, message: refusalText(next) })),\n    });\n  };\n\n  const back = (): void => {\n    setRefusal(Option.none());\n    setStep(previousStep(current));\n  };\n\n  const submit = (): void => {\n    Option.match(guard(step), {\n      onNone: () => {\n        setRefusal(Option.none());\n        onSubmit();\n      },\n      onSome: (next) =>\n        setRefusal(Option.some({ id: step.id, message: refusalText(next) })),\n    });\n  };\n\n  const action = (): ReactNode =>\n    last ? (\n      <Button\n        variant=\"primary\"\n        size=\"md\"\n        testId=\"multi-step-submit\"\n        disabled={pending}\n        onClick={submit}\n      >\n        {submitWord}\n      </Button>\n    ) : (\n      <Button\n        variant=\"primary\"\n        size=\"md\"\n        testId=\"multi-step-next\"\n        disabled={pending}\n        onClick={advance}\n      >\n        {nextWord}\n      </Button>\n    );\n\n  return (\n    <div\n      {...(name === undefined ? {} : { role: \"group\", \"aria-label\": name })}\n      {...rest}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(className), theme), {\n          display: \"flex\",\n          flexDirection: \"column\",\n          gap: theme.spacing.lg,\n        }),\n        style,\n      )}\n    >\n      <Stepper\n        value={current}\n        label={name ?? \"Steps\"}\n        testId=\"multi-step-steps\"\n      >\n        {list.map((entry, at) => (\n          <StepperStep\n            key={entry.id}\n            value={at + 1}\n            label={entry.title}\n            description={entry.description}\n          />\n        ))}\n      </Stepper>\n      <div\n        testId=\"multi-step-content\"\n        role=\"group\"\n        aria-label={stepAnnouncement(title, current, count)}\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          gap: theme.spacing.md,\n        }}\n      >\n        {step?.content}\n      </div>\n      {Option.match(message, {\n        onNone: () => null,\n        onSome: (text) => (\n          <text\n            testId=\"multi-step-error\"\n            role=\"alert\"\n            style={{\n              color: statusTone(theme, \"destructive\").foreground,\n              fontSize: theme.typography.caption.fontSize,\n              lineHeight: theme.typography.caption.lineHeight,\n            }}\n          >\n            {text}\n          </text>\n        ),\n      })}\n      <div\n        style={{\n          display: \"flex\",\n          justifyContent: \"flex-end\",\n          gap: theme.spacing.sm,\n        }}\n      >\n        <Button\n          variant=\"ghost\"\n          size=\"md\"\n          testId=\"multi-step-back\"\n          disabled={pending || current === 1}\n          onClick={back}\n        >\n          {backWord}\n        </Button>\n        {action()}\n      </div>\n    </div>\n  );\n};\n\nexport const multiStepFormMeta = {\n  name: \"Multi-step forms\",\n  tier: 5,\n  status: \"done\",\n  gpuixSupport: \"yes\",\n  limitations: [\n    \"the draft is the caller's; a non-current step's content is unmounted\",\n    \"no step click: the indicator is read-only, like Stepper\",\n    \"the last step's gate runs before onSubmit, not the record decode — that is Form's\",\n  ],\n  docs: \"docs/multi-step-form.mdx\",\n  example: \"examples/multi-step-form.example.tsx\",\n  testFile: \"src/behaviour/multi-step-form.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/stepper.tsx",
      "target": "@ui/gpuixui/components/stepper.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport { accessibleName, visibleCopy } from \"../primitives/core/index.js\";\nimport {\n  cn,\n  STEPPER_DESCRIPTION,\n  STEPPER_LABEL,\n  STEPPER_MARKER,\n  STEPPER_MARKER_TEXT,\n  STEPPER_ORIENTATION,\n  STEPPER_ROOT,\n  STEPPER_STEP,\n  STEPPER_TEXT,\n} from \"../tw/index.js\";\nimport { Match } from \"effect\";\nimport { createContext, useContext } from \"react\";\nimport {\n  assertParts,\n  type PartChildren,\n  type PartComponent,\n  type PartsContract,\n} from \"./internal/parts.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { textOf } from \"./internal/text.js\";\nimport { controlHeight, useTheme, type Theme } from \"./internal/theme.js\";\nimport {\n  currentStep,\n  orientationOf,\n  statusOf,\n  stepName,\n  type StepperOrientation,\n  type StepperStatus,\n} from \"./stepper/logic.js\";\n\nexport type { StepperStatus } from \"./stepper/logic.js\";\n\nexport type { StepperOrientation } from \"./stepper/logic.js\";\n\ninterface StepperContextValue {\n  readonly value: number;\n  readonly orientation: StepperOrientation;\n}\n\nconst StepperContext = createContext<StepperContextValue | undefined>(\n  undefined,\n);\n\nconst useStepper = (): StepperContextValue => {\n  const value = useContext(StepperContext);\n  if (value === undefined) {\n    throw new Error(\"Stepper parts must be used inside Stepper\");\n  }\n  return value;\n};\n\nconst markerPalette = (\n  theme: Theme,\n  status: StepperStatus,\n): { readonly backgroundColor: string; readonly color: string } =>\n  Match.value(status).pipe(\n    Match.when(\"complete\", () => ({\n      backgroundColor: theme.colors.secondary,\n      color: theme.colors.secondaryForeground,\n    })),\n    Match.when(\"current\", () => ({\n      backgroundColor: theme.colors.primary,\n      color: theme.colors.primaryForeground,\n    })),\n    Match.when(\"upcoming\", () => ({\n      backgroundColor: theme.colors.muted,\n      color: theme.colors.mutedForeground,\n    })),\n    Match.exhaustive,\n  );\n\nexport interface StepperProps extends Omit<Props, \"children\"> {\n  readonly value: number;\n\n  readonly orientation?: StepperOrientation;\n\n  readonly label?: string;\n  readonly className?: string;\n  readonly children?: PartChildren;\n}\n\nexport const Stepper = ({\n  value,\n  orientation: requestedOrientation = \"horizontal\",\n  label,\n  className,\n  children,\n  style,\n  ...rest\n}: StepperProps) => {\n  const theme = useTheme();\n\n  const orientation = orientationOf(requestedOrientation);\n\n  const name = accessibleName(textOf(label));\n  assertParts(stepperRoot, children);\n\n  return (\n    <StepperContext.Provider value={{ value: currentStep(value), orientation }}>\n      <div\n        role=\"list\"\n        {...(name === undefined ? {} : { \"aria-label\": name })}\n        {...rest}\n        style={mergeStyles(\n          recipeStyle(\n            cn(STEPPER_ROOT, STEPPER_ORIENTATION[orientation], className),\n            theme,\n          ),\n          style,\n        )}\n      >\n        {children}\n      </div>\n    </StepperContext.Provider>\n  );\n};\n\nexport interface StepperStepProps extends Omit<Props, \"children\" | \"style\"> {\n  readonly value: number;\n\n  readonly label: string;\n\n  readonly description?: string;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<StepperStepState>;\n}\n\nexport interface StepperStepState {\n  readonly status: StepperStatus;\n}\n\nexport const StepperStep = ({\n  value,\n  label,\n  description,\n  className,\n  style,\n  ...rest\n}: StepperStepProps) => {\n  const theme = useTheme();\n  const { value: current } = useStepper();\n  const status = statusOf(current, value);\n  const marker = markerPalette(theme, status);\n  const side = controlHeight(theme, \"md\");\n\n  const name = stepName(label, value);\n  const detail = visibleCopy(textOf(description));\n\n  return (\n    <div\n      role=\"listitem\"\n      aria-label={name}\n      aria-selected={status === \"current\"}\n      {...rest}\n      style={mergeStyles(\n        recipeStyle(cn(STEPPER_STEP, className), theme),\n        resolveStyle(style, { status }),\n      )}\n    >\n      <div\n        style={mergeStyles(recipeStyle(STEPPER_MARKER, theme), {\n          width: side,\n          height: side,\n          backgroundColor: marker.backgroundColor,\n        })}\n      >\n        <text\n          style={mergeStyles(recipeStyle(STEPPER_MARKER_TEXT, theme), {\n            color: marker.color,\n          })}\n        >\n          {String(value)}\n        </text>\n      </div>\n      <div style={recipeStyle(STEPPER_TEXT, theme)}>\n        <text\n          style={mergeStyles(recipeStyle(STEPPER_LABEL, theme), {\n            color:\n              status === \"upcoming\"\n                ? theme.colors.mutedForeground\n                : theme.colors.foreground,\n          })}\n        >\n          {name}\n        </text>\n        {detail === undefined ? null : (\n          <text style={recipeStyle(STEPPER_DESCRIPTION, theme)}>{detail}</text>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst stepperParts: ReadonlyArray<PartComponent> = [Stepper, StepperStep];\n\nconst stepperRoot: PartsContract = {\n  parent: \"Stepper\",\n  family: stepperParts,\n  accepts: [StepperStep],\n  hint: \"Put StepperStep directly inside Stepper.\",\n};\n\nexport const stepperMeta = {\n  name: \"Stepper\",\n  tier: 2,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"no aria-current in 0.11.0; the current step carries aria-selected\",\n    \"display-only by design: there is no step click, disabled policy or onValueChange\",\n    \"no connector line between steps\",\n  ],\n  docs: \"docs/stepper.mdx\",\n  example: \"examples/stepper.example.tsx\",\n  testFile: \"src/behaviour/stepper.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/stepper/logic.ts",
      "target": "@ui/gpuixui/components/stepper/logic.ts",
      "type": "registry:lib",
      "content": "import { accessibleName } from \"../../primitives/core/index.js\";\nimport * as Schema from \"effect/Schema\";\nimport { textOf } from \"../internal/text.js\";\n\nexport type StepperStatus = \"complete\" | \"current\" | \"upcoming\";\n\nexport const StepperOrientations = Schema.Literals([\"horizontal\", \"vertical\"]);\n\nexport type StepperOrientation = typeof StepperOrientations.Type;\n\nexport const orientationOf = (value: unknown): StepperOrientation =>\n  Schema.is(StepperOrientations)(value) ? value : \"horizontal\";\n\nexport const stepName = (title: unknown, position: number): string =>\n  accessibleName(textOf(title)) ??\n  `Step ${Number.isFinite(position) ? Math.max(1, Math.floor(position)) : 1}`;\n\nexport const currentStep = (value: number): number =>\n  Number.isFinite(value) ? value : 1;\n\nexport const statusOf = (current: number, step: number): StepperStatus => {\n  const at = currentStep(current);\n  return step < at ? \"complete\" : step === at ? \"current\" : \"upcoming\";\n};\n"
    },
    {
      "path": "packages/components/src/wizard/logic.ts",
      "target": "@ui/gpuixui/components/wizard/logic.ts",
      "type": "registry:lib",
      "content": "import { spokenSentence } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\n\nexport interface StepGate {\n  readonly gate?: () => Option.Option<string>;\n}\n\nexport interface StepGateSource {\n  readonly gate?: unknown;\n}\n\nconst isStep = <Step extends { readonly id: string }>(\n  entry: unknown,\n): entry is Step => {\n  if (typeof entry !== \"object\" || entry === null || !(\"id\" in entry)) {\n    return false;\n  }\n  const id = entry.id;\n  return typeof id === \"string\" && id.length > 0;\n};\n\nexport const stepsOf = <Step extends { readonly id: string }>(\n  value: ReadonlyArray<Step> | null | undefined,\n): ReadonlyArray<Step> => {\n  const list = Option.getOrElse(\n    Option.filter(Option.fromNullishOr(value), (entries): boolean =>\n      Arr.isArray(entries),\n    ),\n    () => [],\n  );\n  return Arr.filter(list, isStep);\n};\n\nexport const stepOf = (value: number, count: number): number => {\n  if (count < 1) return 1;\n  const named = Number.isFinite(value) ? Math.floor(value) : 1;\n  return Math.min(Math.max(named, 1), count);\n};\n\nexport const nextStep = (step: number, count: number): number =>\n  Math.min(stepOf(step, count) + 1, Math.max(1, count));\n\nexport const previousStep = (step: number): number =>\n  Number.isFinite(step) ? Math.max(1, Math.floor(step) - 1) : 1;\n\nexport const isLastStep = (step: number, count: number): boolean =>\n  count > 0 && stepOf(step, count) === count;\n\nexport const gateMessage = (\n  step: StepGateSource | null | undefined,\n): Option.Option<string> => {\n  const gate = step?.gate;\n  return typeof gate === \"function\" ? gate() : Option.none();\n};\n\nexport const REFUSAL_FALLBACK = \"This step is not complete yet.\";\n\nexport const refusalText = (message: string): string =>\n  spokenSentence(message, REFUSAL_FALLBACK);\n\nexport const stepAnnouncement = (\n  title: string,\n  step: number,\n  count: number,\n): string => `${title}, step ${stepOf(step, count)} of ${Math.max(1, count)}`;\n"
    }
  ],
  "docs": "[MultiStepForm](https://gpuixui.sudokw.com/components/multi-step-form) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/multi-step-form",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}