{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "onboarding",
  "type": "registry:ui",
  "description": "Composable multi-step onboarding primitives: Onboarding root with step navigation, FeatureCarousel, ChoiceGroup radio selector, TipsList, and StepIndicator with dots and pills variants",
  "dependencies": [
    "@radix-ui/react-use-controllable-state",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/default/ui/onboarding.tsx",
      "content": "\"use client\"\n\nimport type * as React from \"react\"\nimport {\n  Children,\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  type PropsWithChildren,\n} from \"react\"\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\nconst stepIndicatorVariants = cva(\"flex items-center justify-center gap-2\", {\n  variants: {\n    variant: {\n      dots: \"\",\n      pills: \"\",\n    },\n  },\n  defaultVariants: {\n    variant: \"dots\",\n  },\n})\n\nconst stepDotVariants = cva(\"rounded-full transition-all duration-200\", {\n  variants: {\n    variant: {\n      dots: \"data-[state=active]:bg-foreground data-[state=completed]:bg-foreground/60 data-[state=inactive]:bg-muted-foreground/30 size-2 data-[state=active]:size-2.5\",\n      pills:\n        \"data-[state=active]:bg-foreground data-[state=completed]:bg-foreground/60 data-[state=inactive]:bg-muted-foreground/30 h-1 max-w-8 flex-1 rounded-full\",\n    },\n  },\n  defaultVariants: {\n    variant: \"dots\",\n  },\n})\n\nexport interface StepIndicatorProps\n  extends React.ComponentPropsWithoutRef<\"div\">,\n    VariantProps<typeof stepIndicatorVariants>,\n    VariantProps<typeof stepDotVariants> {\n  /** Current step index (1-based) */\n  currentStep: number\n  /** Total number of steps */\n  totalSteps: number\n  /** Optional className for each step dot */\n  dotClassName?: string\n}\n\n/**\n * Headless step indicator primitive.\n * Renders a list of step dots with proper ARIA for progress indication.\n * No visual styling—consumer provides via className.\n */\nexport function StepIndicator({\n  currentStep,\n  totalSteps,\n  variant = \"dots\",\n  className,\n  dotClassName,\n  ...props\n}: StepIndicatorProps) {\n  return (\n    <div\n      aria-label={`Step ${currentStep} of ${totalSteps}`}\n      aria-valuemax={totalSteps}\n      aria-valuemin={1}\n      aria-valuenow={currentStep}\n      className={cn(stepIndicatorVariants({ variant }), className)}\n      data-slot=\"onboarding-step-indicator\"\n      role=\"progressbar\"\n      {...props}\n    >\n      {Array.from({ length: totalSteps }, (_, i) => {\n        const stepNumber = i + 1\n        const isActive = currentStep === stepNumber\n        const isCompleted = currentStep > stepNumber\n        let stepState: \"active\" | \"completed\" | \"inactive\" = \"inactive\"\n        if (isActive) {\n          stepState = \"active\"\n        } else if (isCompleted) {\n          stepState = \"completed\"\n        }\n        return (\n          <div\n            aria-current={isActive ? \"step\" : undefined}\n            className={cn(stepDotVariants({ variant }), dotClassName)}\n            data-slot=\"onboarding-step-dot\"\n            data-state={stepState}\n            key={stepNumber}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface OnboardingContextValue {\n  /** Current step index (1-based) */\n  currentStep: number\n  /** Total number of steps */\n  totalSteps: number\n  /** Sub-step value (e.g. feature carousel index within step 1) */\n  stepValue: number\n  /** Set current step */\n  setStep: (step: number | ((prev: number) => number)) => void\n  /** Set step value (sub-step) */\n  setStepValue: (value: number | ((prev: number) => number)) => void\n  /** Max step value for current step (e.g. feature count - 1) */\n  maxStepValue: number\n  /** Whether user can proceed to next */\n  canGoNext: boolean\n  /** Whether user can go back */\n  canGoBack: boolean\n  /** Navigate to previous step */\n  handleBack: () => void\n  /** Navigate to next step or advance sub-step */\n  handleNext: () => void\n  /** Complete onboarding */\n  handleComplete: () => void\n  /** Callback when onboarding is completed */\n  onComplete?: () => void\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst OnboardingContext = createContext<OnboardingContextValue | null>(null)\n\nfunction useOnboarding() {\n  const ctx = useContext(OnboardingContext)\n  if (!ctx) {\n    throw new Error(\"Onboarding components must be used within Onboarding.Root\")\n  }\n  return ctx\n}\n\n// ============================================================================\n// Root\n// ============================================================================\n\nexport interface OnboardingRootProps\n  extends PropsWithChildren,\n    Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** Controlled step index (1-based) */\n  value?: number\n  /** Default step index (uncontrolled) */\n  defaultValue?: number\n  /** Callback when step changes */\n  onValueChange?: (step: number) => void\n  /** Controlled sub-step value */\n  stepValue?: number\n  /** Default sub-step value (uncontrolled) */\n  defaultStepValue?: number\n  /** Callback when sub-step value changes */\n  onStepValueChange?: (value: number) => void\n  /** Total number of steps */\n  totalSteps: number\n  /** Max sub-step value for step 1 (e.g. feature count - 1). Default 0 = no sub-steps */\n  maxStepValue?: number\n  /** Callback when onboarding is completed */\n  onComplete?: () => void\n  /** Custom logic for whether user can proceed. Receives (step, stepValue). Default: true */\n  canGoNext?: (step: number, stepValue: number) => boolean\n}\n\nfunction OnboardingRoot({\n  value: controlledValue,\n  defaultValue = 1,\n  onValueChange,\n  stepValue: controlledStepValue,\n  defaultStepValue = 0,\n  onStepValueChange,\n  totalSteps,\n  maxStepValue: controlledMaxStepValue = 0,\n  onComplete,\n  canGoNext: canGoNextFn,\n  children,\n  className,\n  ...props\n}: OnboardingRootProps) {\n  const [currentStep, setCurrentStep] = useControllableState({\n    prop: controlledValue,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n  })\n\n  const [stepValue, setStepValueState] = useControllableState({\n    prop: controlledStepValue,\n    defaultProp: defaultStepValue,\n    onChange: onStepValueChange,\n  })\n\n  const maxStepValue = controlledMaxStepValue ?? 0\n\n  const canGoNext = canGoNextFn ? canGoNextFn(currentStep, stepValue) : true\n\n  const canGoBack = currentStep > 1 || stepValue > 0\n\n  const handleNext = useCallback(() => {\n    if (currentStep === 1 && stepValue < maxStepValue) {\n      setStepValueState((prev) => prev + 1)\n    } else if (currentStep < totalSteps) {\n      setStepValueState(0)\n      setCurrentStep((prev) => prev + 1)\n    }\n  }, [\n    currentStep,\n    stepValue,\n    maxStepValue,\n    totalSteps,\n    setStepValueState,\n    setCurrentStep,\n  ])\n\n  const handleBack = useCallback(() => {\n    if (currentStep === 1 && stepValue > 0) {\n      setStepValueState((prev) => prev - 1)\n    } else if (currentStep === 2) {\n      setCurrentStep(1)\n      setStepValueState(maxStepValue)\n    } else if (currentStep > 1) {\n      setCurrentStep((prev) => prev - 1)\n    }\n  }, [currentStep, stepValue, maxStepValue, setStepValueState, setCurrentStep])\n\n  const handleComplete = useCallback(() => {\n    onComplete?.()\n  }, [onComplete])\n\n  const contextValue = useMemo<OnboardingContextValue>(\n    () => ({\n      currentStep,\n      totalSteps,\n      stepValue,\n      setStep: setCurrentStep,\n      setStepValue: setStepValueState,\n      maxStepValue,\n      canGoNext,\n      canGoBack,\n      handleBack,\n      handleNext,\n      handleComplete,\n      onComplete,\n    }),\n    [\n      currentStep,\n      totalSteps,\n      stepValue,\n      setCurrentStep,\n      setStepValueState,\n      maxStepValue,\n      canGoNext,\n      canGoBack,\n      handleBack,\n      handleNext,\n      handleComplete,\n      onComplete,\n    ]\n  )\n\n  return (\n    <OnboardingContext.Provider value={contextValue}>\n      <div\n        className={cn(\n          \"bg-background flex flex-col rounded-xl border p-6 shadow-sm\",\n          className\n        )}\n        data-slot=\"onboarding\"\n        data-state={`step-${currentStep}`}\n        {...props}\n      >\n        {children}\n      </div>\n    </OnboardingContext.Provider>\n  )\n}\n\n// ============================================================================\n// Step\n// ============================================================================\n\nexport interface OnboardingStepProps\n  extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Step index (1-based) - content renders when currentStep matches */\n  step: number\n}\n\nfunction OnboardingStep({\n  step,\n  children,\n  className,\n  ...props\n}: OnboardingStepProps) {\n  const { currentStep } = useOnboarding()\n  const isActive = currentStep === step\n\n  if (!isActive) {\n    return null\n  }\n\n  return (\n    <div\n      className={cn(className)}\n      data-slot=\"onboarding-step\"\n      data-state=\"active\"\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\n// ============================================================================\n// StepIndicator\n// ============================================================================\n\nexport interface OnboardingStepIndicatorProps\n  extends Omit<\n    React.ComponentProps<typeof StepIndicator>,\n    \"currentStep\" | \"totalSteps\"\n  > {}\n\nfunction OnboardingStepIndicator(props: OnboardingStepIndicatorProps) {\n  const { currentStep, totalSteps } = useOnboarding()\n  return (\n    <StepIndicator\n      currentStep={currentStep}\n      totalSteps={totalSteps}\n      {...props}\n    />\n  )\n}\n\n// ============================================================================\n// Header\n// ============================================================================\n\nexport interface OnboardingHeaderProps\n  extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Step title (optional when using children) */\n  title?: string\n  /** Step description */\n  description?: string\n  /** Custom header content (overrides title/description) */\n  children?: React.ReactNode\n}\n\nfunction OnboardingHeader({\n  title,\n  description,\n  children,\n  className,\n  ...props\n}: OnboardingHeaderProps) {\n  if (children) {\n    return (\n      <div\n        className={cn(\"text-center\", className)}\n        data-slot=\"onboarding-header\"\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-1 text-center\",\n        \"[&_[data-slot=onboarding-title]]:text-foreground [&_[data-slot=onboarding-title]]:font-serif [&_[data-slot=onboarding-title]]:text-3xl [&_[data-slot=onboarding-title]]:font-normal\",\n        \"[&_[data-slot=onboarding-description]]:text-muted-foreground [&_[data-slot=onboarding-description]]:text-base\",\n        className\n      )}\n      data-slot=\"onboarding-header\"\n      {...props}\n    >\n      {title != null && <h2 data-slot=\"onboarding-title\">{title}</h2>}\n      {description && <p data-slot=\"onboarding-description\">{description}</p>}\n    </div>\n  )\n}\n\n// ============================================================================\n// Navigation\n// ============================================================================\n\nexport interface OnboardingNavigationProps\n  extends React.ComponentPropsWithoutRef<\"fieldset\"> {\n  /** Back button label */\n  backLabel?: string\n  /** Next button label */\n  nextLabel?: string\n  /** Complete button label */\n  completeLabel?: string\n  /** Override can go next (when not using Root's canGoNext) */\n  canGoNext?: boolean\n  /** Custom navigation content (use with asChild for full control) */\n  children?: React.ReactNode\n}\n\nfunction OnboardingNavigation({\n  backLabel = \"Back\",\n  nextLabel = \"Next\",\n  completeLabel = \"Start Creating\",\n  canGoNext: canGoNextOverride,\n  children,\n  className,\n  ...props\n}: OnboardingNavigationProps) {\n  const {\n    currentStep,\n    totalSteps,\n    canGoNext: contextCanGoNext,\n    canGoBack,\n    handleBack,\n    handleNext,\n    handleComplete,\n  } = useOnboarding()\n\n  const canGoNext = canGoNextOverride ?? contextCanGoNext\n  const isLastStep = currentStep === totalSteps\n\n  if (children) {\n    return (\n      <fieldset\n        className={cn(\"flex gap-3\", className)}\n        data-slot=\"onboarding-navigation\"\n        {...props}\n      >\n        {children}\n      </fieldset>\n    )\n  }\n\n  return (\n    <fieldset\n      aria-label=\"Onboarding navigation\"\n      className={cn(\"flex gap-3\", className)}\n      data-slot=\"onboarding-navigation\"\n      {...props}\n    >\n      <Button\n        aria-label={backLabel}\n        className=\"flex-1 rounded-xl py-5\"\n        data-slot=\"onboarding-back\"\n        disabled={!canGoBack}\n        onClick={handleBack}\n        variant=\"outline\"\n      >\n        {backLabel}\n      </Button>\n      {isLastStep ? (\n        <Button\n          aria-label={completeLabel}\n          className=\"bg-foreground text-background hover:bg-foreground/90 flex-1 rounded-xl py-5\"\n          data-slot=\"onboarding-complete\"\n          onClick={handleComplete}\n        >\n          {completeLabel}\n        </Button>\n      ) : (\n        <Button\n          aria-label={nextLabel}\n          className=\"bg-foreground text-background hover:bg-foreground/90 flex-1 rounded-xl py-5\"\n          data-slot=\"onboarding-next\"\n          disabled={!canGoNext}\n          onClick={handleNext}\n        >\n          {nextLabel}\n        </Button>\n      )}\n    </fieldset>\n  )\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\ntype Orientation = \"horizontal\" | \"vertical\" | \"grid\"\n\ninterface ChoiceGroupContextValue {\n  value: string | null\n  setValue: (value: string) => void\n  name: string\n  orientation: Orientation\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst ChoiceGroupContext = createContext<ChoiceGroupContextValue | null>(null)\n\nfunction useChoiceGroup() {\n  const ctx = useContext(ChoiceGroupContext)\n  if (!ctx) {\n    throw new Error(\"ChoiceGroup.Item must be used within ChoiceGroup\")\n  }\n  return ctx\n}\n\n// ============================================================================\n// Root\n// ============================================================================\n\nexport interface ChoiceGroupProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"defaultValue\"> {\n  /** Controlled selected value */\n  value?: string | null\n  /** Default selected value (uncontrolled) */\n  defaultValue?: string | null\n  /** Callback when selection changes */\n  onValueChange?: (value: string) => void\n  /** Name for radio group semantics (required for accessibility) */\n  name: string\n  /** Layout orientation */\n  orientation?: Orientation\n}\n\nfunction ChoiceGroupRoot({\n  value: controlledValue,\n  defaultValue = null,\n  onValueChange,\n  name,\n  orientation = \"grid\",\n  children,\n  className,\n  ...props\n}: ChoiceGroupProps) {\n  const [value, setValueState] = useControllableState({\n    prop: controlledValue ?? undefined,\n    defaultProp: defaultValue ?? null,\n    onChange: (v) => v !== null && onValueChange?.(v),\n  })\n\n  const setValue = useCallback(\n    (v: string) => {\n      setValueState(v)\n    },\n    [setValueState]\n  )\n\n  const contextValue = useMemo<ChoiceGroupContextValue>(\n    () => ({\n      value,\n      setValue,\n      name,\n      orientation,\n    }),\n    [value, setValue, name, orientation]\n  )\n\n  return (\n    <ChoiceGroupContext.Provider value={contextValue}>\n      <div\n        aria-label={name}\n        className={cn(className)}\n        data-orientation={orientation}\n        data-slot=\"choice-group\"\n        role=\"radiogroup\"\n        {...props}\n      >\n        {children}\n      </div>\n    </ChoiceGroupContext.Provider>\n  )\n}\n\n// ============================================================================\n// Item\n// ============================================================================\n\nexport interface ChoiceGroupItemProps\n  extends React.ComponentPropsWithoutRef<\"label\"> {\n  /** Value when this item is selected */\n  value: string\n}\n\nfunction ChoiceGroupItemComponent({\n  value: itemValue,\n  children,\n  className,\n  ...props\n}: ChoiceGroupItemProps) {\n  const { value, setValue, name } = useChoiceGroup()\n  const isSelected = value === itemValue\n\n  const handleChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      if (e.currentTarget.checked) {\n        setValue(itemValue)\n      }\n    },\n    [itemValue, setValue]\n  )\n\n  return (\n    <label\n      className={cn(className)}\n      data-slot=\"choice-group-item\"\n      data-state={isSelected ? \"selected\" : \"unselected\"}\n      {...props}\n    >\n      <input\n        checked={isSelected}\n        className=\"sr-only\"\n        name={name}\n        onChange={handleChange}\n        type=\"radio\"\n        value={itemValue}\n      />\n      {children}\n    </label>\n  )\n}\n\nChoiceGroupItemComponent.displayName = \"ChoiceGroupItem\"\n\n// ============================================================================\n// Export\n// ============================================================================\n\nexport const ChoiceGroup = Object.assign(ChoiceGroupRoot, {\n  Item: ChoiceGroupItemComponent,\n})\n\n// ============================================================================\n// Types\n// ============================================================================\n\ninterface FeatureCarouselContextValue {\n  value: number\n  setValue: (value: number | ((prev: number) => number)) => void\n  totalItems: number\n  isActive: (index: number) => boolean\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst FeatureCarouselContext =\n  createContext<FeatureCarouselContextValue | null>(null)\n\nfunction useFeatureCarousel() {\n  const ctx = useContext(FeatureCarouselContext)\n  if (!ctx) {\n    throw new Error(\"FeatureCarousel.Item must be used within FeatureCarousel\")\n  }\n  return ctx\n}\n\n// ============================================================================\n// Root\n// ============================================================================\n\nexport interface FeatureCarouselProps\n  extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Controlled active index */\n  value?: number\n  /** Default active index (uncontrolled) */\n  defaultValue?: number\n  /** Callback when active index changes */\n  onValueChange?: (index: number) => void\n  /** Total number of items (derived from children if not provided) */\n  totalItems?: number\n}\n\nfunction FeatureCarouselRoot({\n  value: controlledValue,\n  defaultValue = 0,\n  onValueChange,\n  totalItems: totalItemsProp,\n  children,\n  className,\n  ...props\n}: FeatureCarouselProps) {\n  const [value, setValue] = useControllableState({\n    prop: controlledValue,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n  })\n\n  const totalItems = totalItemsProp ?? Children.count(children)\n\n  const isActive = useCallback((index: number) => value === index, [value])\n\n  const contextValue = useMemo<FeatureCarouselContextValue>(\n    () => ({\n      value,\n      setValue,\n      totalItems,\n      isActive,\n    }),\n    [value, setValue, totalItems, isActive]\n  )\n\n  return (\n    <FeatureCarouselContext.Provider value={contextValue}>\n      <div\n        aria-label=\"Features\"\n        className={cn(className)}\n        data-slot=\"feature-carousel\"\n        role=\"tablist\"\n        {...props}\n      >\n        {children}\n      </div>\n    </FeatureCarouselContext.Provider>\n  )\n}\n\n// ============================================================================\n// Item\n// ============================================================================\n\nexport interface FeatureCarouselItemProps\n  extends React.ComponentPropsWithoutRef<\"button\"> {\n  /** Index of this item (0-based) */\n  index: number\n}\n\nfunction FeatureCarouselItemComponent({\n  index,\n  children,\n  className,\n  onClick,\n  ...props\n}: FeatureCarouselItemProps) {\n  const { setValue, isActive, totalItems } = useFeatureCarousel()\n  const active = isActive(index)\n\n  const handleClick = useCallback(\n    (e: React.MouseEvent<HTMLButtonElement>) => {\n      setValue(index)\n      onClick?.(e)\n    },\n    [index, setValue, onClick]\n  )\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (totalItems <= 1) {\n        return\n      }\n      if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n        e.preventDefault()\n        setValue((prev) => Math.min(prev + 1, totalItems - 1))\n      } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n        e.preventDefault()\n        setValue((prev) => Math.max(prev - 1, 0))\n      }\n    },\n    [totalItems, setValue]\n  )\n\n  return (\n    <button\n      aria-selected={active}\n      className={cn(className)}\n      data-slot=\"feature-carousel-item\"\n      data-state={active ? \"active\" : \"inactive\"}\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      role=\"tab\"\n      tabIndex={active ? 0 : -1}\n      type=\"button\"\n      {...props}\n    >\n      {children}\n    </button>\n  )\n}\n\nFeatureCarouselItemComponent.displayName = \"FeatureCarouselItem\"\n\n// ============================================================================\n// Export\n// ============================================================================\n\nexport const FeatureCarousel = Object.assign(FeatureCarouselRoot, {\n  Item: FeatureCarouselItemComponent,\n})\n\n// ============================================================================\n// TipsList\n// ============================================================================\n\nexport interface TipsListProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Optional title/label for the list */\n  title?: string\n}\n\n/**\n * Headless tips list primitive.\n * Renders an ordered list with optional title.\n * No visual styling—consumer provides via className.\n */\nfunction TipsListRoot({ title, children, className, ...props }: TipsListProps) {\n  const titleId = useId()\n  return (\n    <div className={cn(className)} data-slot=\"tips-list\" {...props}>\n      {title && (\n        <p className=\"sr-only\" data-slot=\"tips-list-title\" id={titleId}>\n          {title}\n        </p>\n      )}\n      <ol\n        aria-label={title ? undefined : \"Tips\"}\n        aria-labelledby={title ? titleId : undefined}\n        data-slot=\"tips-list-items\"\n      >\n        {children}\n      </ol>\n    </div>\n  )\n}\n\n// ============================================================================\n// Item\n// ============================================================================\n\nexport interface TipsListItemProps\n  extends React.ComponentPropsWithoutRef<\"li\"> {\n  /** Optional number to display (for custom styling) */\n  number?: number\n}\n\nfunction TipsListItemComponent({\n  number,\n  children,\n  className,\n  ...props\n}: TipsListItemProps) {\n  return (\n    <li\n      className={cn(className)}\n      data-number={number}\n      data-slot=\"tips-list-item\"\n      {...props}\n    >\n      {number != null && (\n        <span aria-hidden data-slot=\"tips-list-item-number\">\n          {number}\n        </span>\n      )}\n      {children}\n    </li>\n  )\n}\n\n// ============================================================================\n// Export\n// ============================================================================\n\nexport const TipsList = Object.assign(TipsListRoot, {\n  Item: TipsListItemComponent,\n})\n\n// ============================================================================\n// Export\n// ============================================================================\n\nexport const Onboarding = Object.assign(OnboardingRoot, {\n  Step: OnboardingStep,\n  StepIndicator: OnboardingStepIndicator,\n  Header: OnboardingHeader,\n  Navigation: OnboardingNavigation,\n})\n\nexport { useOnboarding }\n",
      "type": "registry:ui"
    }
  ]
}