{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "stepper-glass", "type": "registry:ui", "title": "Stepper Glass", "description": "StepperGlass Component (Compound API)", "dependencies": [ "lucide-react", "shadcn-glass-ui" ], "registryDependencies": [ "cn", "use-focus", "variants" ], "files": [ { "path": "components/glass/ui/stepper-glass.tsx", "type": "registry:component", "content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * StepperGlass Component (Compound API)\n *\n * Glass-themed step indicator for multi-step workflows with theme-aware styling\n * and full accessibility support.\n *\n * ## Features\n * - Theme-aware glassmorphism styling (glass/light/aurora)\n * - Horizontal and vertical orientations\n * - Three visual variants: numbered, icon, dots\n * - Three sizes: small, medium, large\n * - Linear mode to lock future steps\n * - Animated connector lines between steps\n * - Compound component API for maximum flexibility\n * - Keyboard navigation with arrow keys\n * - 44x44px minimum touch targets (WCAG 2.5.5)\n * - Custom icons and completed icon overrides\n *\n * ## Sub-Components\n * - **StepperGlass.Root** - Context provider with value/onValueChange\n * - **StepperGlass.List** - Visual container for step triggers (uses `role=\"tablist\"`)\n * - **StepperGlass.Step** - Individual step button with indicator and label\n * - **StepperGlass.Content** - Content panel for each step (uses `role=\"tabpanel\"`)\n *\n * ## CSS Variables\n * Customize appearance via theme CSS variables:\n * - `--stepper-step-bg` - Pending step background\n * - `--stepper-step-active-bg` - Active step background\n * - `--stepper-step-completed-bg` - Completed step background (purple gradient)\n * - `--stepper-step-disabled-bg` - Disabled step background\n * - `--stepper-step-border` - Step indicator border\n * - `--stepper-step-active-border` - Active step border with subtle glow\n * - `--stepper-step-completed-border` - Completed step border\n * - `--stepper-step-disabled-border` - Disabled step border (muted)\n * - `--stepper-step-text` - Step number/icon text color\n * - `--stepper-step-active-text` - Active step text (purple)\n * - `--stepper-step-completed-text` - Completed step text (white)\n * - `--stepper-step-disabled-text` - Disabled step text (muted)\n * - `--stepper-connector-bg` - Connector line background (default)\n * - `--stepper-connector-active-bg` - Connector line background (completed, purple)\n * - `--stepper-step-glow` - Completed step glow effect\n * - `--stepper-step-active-glow` - Active step glow effect\n * - `--stepper-label-text` - Label text color\n * - `--stepper-description-text` - Description text color (subtle)\n *\n * @example Basic usage (numbered variant)\n * ```tsx\n * import { StepperGlass } from 'shadcn-glass-ui'\n *\n * function Wizard() {\n * const [step, setStep] = useState('step1')\n *\n * return (\n * \n * \n * \n * \n * \n * \n * Step 1 content\n * Step 2 content\n * Step 3 content\n * \n * )\n * }\n * ```\n *\n * @example Icon variant with custom icons\n * ```tsx\n * \n * \n * } />\n * } />\n * } />\n * \n * \n * ```\n *\n * @example Linear mode (lock future steps)\n * ```tsx\n * \n * \n * \n * \n * \n * \n * \n * ```\n *\n * @example Vertical orientation\n * ```tsx\n * \n * \n * \n * \n * \n * \n * \n * ```\n *\n * @accessibility\n * - **Keyboard Navigation:** Arrow keys navigate between steps (WCAG 2.1.1)\n * - **Focus Management:** Visible focus ring using `--focus-glow` (WCAG 2.4.7)\n * - **Screen Readers:** Uses `role=\"tablist\"`, `role=\"tab\"` (WCAG 4.1.3)\n * - **ARIA Attributes:** `aria-current=\"step\"`, `aria-disabled` for state\n * - **Touch Targets:** 44x44px minimum touch targets (WCAG 2.5.5)\n * - **Color Contrast:** All states meet WCAG AA 4.5:1 ratio\n * - **Motion:** Connector line animations respect `prefers-reduced-motion`\n *\n * @since v1.0.0\n */\n\nimport {\n forwardRef,\n createContext,\n useContext,\n useMemo,\n useState,\n useCallback,\n useLayoutEffect,\n type CSSProperties,\n type FC,\n type ReactNode,\n} from 'react';\nimport { cn } from '@/lib/utils';\nimport { useFocus } from '@/lib/hooks/use-focus';\nimport { Check } from 'lucide-react';\nimport {\n stepperRootVariants,\n stepperListVariants,\n stepperStepContainerVariants,\n stepperIndicatorVariants,\n stepperConnectorVariants,\n stepperLabelVariants,\n stepperDescriptionVariants,\n stepperContentVariants,\n type StepperOrientation,\n type StepperVariant,\n type StepperSize,\n type StepStatus,\n} from '@/lib/variants/stepper-glass-variants';\nimport '@/glass-theme.css';\n\n// ========================================\n// CONTEXT\n// ========================================\n\ninterface StepperContextValue {\n value: string;\n onValueChange?: (value: string) => void;\n orientation: StepperOrientation;\n variant: StepperVariant;\n size: StepperSize;\n linear: boolean;\n steps: string[];\n registerStep: (value: string, index: number) => void;\n unregisterStep: (value: string) => void;\n}\n\nconst StepperContext = createContext(null);\n\nconst useStepperContext = () => {\n const context = useContext(StepperContext);\n if (!context) {\n throw new Error('Stepper compound components must be used within StepperGlass.Root');\n }\n return context;\n};\n\n// ========================================\n// UTILITY: GET STEP STATUS\n// ========================================\n\nfunction getStepStatus(\n stepValue: string,\n currentValue: string,\n steps: string[],\n linear: boolean,\n disabled?: boolean\n): StepStatus {\n if (disabled) return 'disabled';\n\n const stepIndex = steps.indexOf(stepValue);\n const currentIndex = steps.indexOf(currentValue);\n\n if (stepIndex === -1 || currentIndex === -1) return 'pending';\n if (stepIndex === currentIndex) return 'active';\n if (stepIndex < currentIndex) return 'completed';\n if (linear && stepIndex > currentIndex) return 'disabled';\n return 'pending';\n}\n\n// ========================================\n// ROOT COMPONENT\n// ========================================\n\n/**\n * Props for StepperGlass.Root component.\n *\n * @example\n * ```tsx\n * const props: StepperRootProps = {\n * value: 'step2',\n * onValueChange: (value) => setStep(value),\n * orientation: 'horizontal',\n * variant: 'numbered',\n * size: 'md',\n * linear: false,\n * };\n * ```\n */\ninterface StepperRootProps {\n /** Current active step value */\n value: string;\n /** Callback when step value changes */\n onValueChange?: (value: string) => void;\n /**\n * Orientation of the stepper.\n *\n * @default \"horizontal\"\n */\n orientation?: StepperOrientation;\n /**\n * Visual variant.\n *\n * @default \"numbered\"\n */\n variant?: StepperVariant;\n /**\n * Size of step indicators.\n *\n * @default \"md\"\n */\n size?: StepperSize;\n /**\n * Lock future steps (require sequential completion).\n *\n * @default false\n */\n linear?: boolean;\n /** Child components */\n children: ReactNode;\n /** Optional className */\n className?: string;\n}\n\nconst StepperRoot: FC = ({\n value,\n onValueChange,\n orientation = 'horizontal',\n variant = 'numbered',\n size = 'md',\n linear = false,\n children,\n className,\n}) => {\n const [steps, setSteps] = useState([]);\n\n const registerStep = useCallback((stepValue: string, index: number) => {\n setSteps((prev) => {\n if (prev.includes(stepValue)) return prev;\n const newSteps = [...prev];\n // Insert at correct position to maintain order\n newSteps.splice(index, 0, stepValue);\n return newSteps;\n });\n }, []);\n\n const unregisterStep = useCallback((stepValue: string) => {\n setSteps((prev) => prev.filter((s) => s !== stepValue));\n }, []);\n\n const contextValue = useMemo(\n () => ({\n value,\n onValueChange,\n orientation,\n variant,\n size,\n linear,\n steps,\n registerStep,\n unregisterStep,\n }),\n [value, onValueChange, orientation, variant, size, linear, steps, registerStep, unregisterStep]\n );\n\n return (\n \n \n {children}\n \n \n );\n};\n\n// ========================================\n// LIST COMPONENT\n// ========================================\n\ninterface StepperListProps extends React.HTMLAttributes {\n children: ReactNode;\n className?: string;\n}\n\nconst StepperList = forwardRef(\n ({ children, className, ...props }, ref) => {\n const { orientation } = useStepperContext();\n\n return (\n \n {children}\n \n );\n }\n);\n\nStepperList.displayName = 'StepperList';\n\n// ========================================\n// STEP COMPONENT\n// ========================================\n\ninterface StepperStepProps {\n /** Unique value for this step */\n value: string;\n /** Step label (required for accessibility) */\n label: string;\n /** Optional description */\n description?: string;\n /** Custom icon (for icon variant) */\n icon?: ReactNode;\n /** Completed icon override */\n completedIcon?: ReactNode;\n /** Force disabled state */\n disabled?: boolean;\n /** Optional className */\n className?: string;\n /** Step index for ordering (auto-detected) */\n index?: number;\n}\n\nconst StepperStep = forwardRef(\n (\n {\n value: stepValue,\n label,\n description,\n icon,\n completedIcon,\n disabled: forcedDisabled,\n className,\n index: providedIndex,\n },\n ref\n ) => {\n const {\n value: currentValue,\n onValueChange,\n orientation,\n variant,\n size,\n linear,\n steps,\n registerStep,\n unregisterStep,\n } = useStepperContext();\n\n const { isFocusVisible, focusProps } = useFocus({ focusVisible: true });\n\n // Track mount order for step registration\n const [mountIndex] = useState(() => providedIndex ?? Date.now());\n\n // Register step on mount\n // Use useLayoutEffect to register before paint\n useLayoutEffect(() => {\n registerStep(stepValue, mountIndex);\n return () => unregisterStep(stepValue);\n }, [stepValue, mountIndex, registerStep, unregisterStep]);\n\n const status = getStepStatus(stepValue, currentValue, steps, linear, forcedDisabled);\n const stepIndex = steps.indexOf(stepValue);\n const isLast = stepIndex === steps.length - 1;\n const isClickable = status !== 'disabled';\n\n // Styles based on status\n const indicatorStyles: CSSProperties = {\n background:\n status === 'completed'\n ? 'var(--stepper-step-completed-bg)'\n : status === 'active'\n ? 'var(--stepper-step-active-bg)'\n : status === 'disabled'\n ? 'var(--stepper-step-disabled-bg)'\n : 'var(--stepper-step-bg)',\n border: `2px solid ${\n status === 'completed'\n ? 'var(--stepper-step-completed-border)'\n : status === 'active'\n ? 'var(--stepper-step-active-border)'\n : status === 'disabled'\n ? 'var(--stepper-step-disabled-border)'\n : 'var(--stepper-step-border)'\n }`,\n color:\n status === 'completed'\n ? 'var(--stepper-step-completed-text)'\n : status === 'active'\n ? 'var(--stepper-step-active-text)'\n : status === 'disabled'\n ? 'var(--stepper-step-disabled-text)'\n : 'var(--stepper-step-text)',\n boxShadow:\n status === 'active'\n ? 'var(--stepper-step-active-glow)'\n : status === 'completed'\n ? 'var(--stepper-step-glow)'\n : isFocusVisible\n ? 'var(--focus-glow)'\n : 'none',\n backdropFilter: 'blur(var(--blur-sm))',\n };\n\n const connectorStyles: CSSProperties = {\n background:\n stepIndex < steps.indexOf(currentValue)\n ? 'var(--stepper-connector-active-bg)'\n : 'var(--stepper-connector-bg)',\n };\n\n const labelStyles: CSSProperties = {\n color:\n status === 'active' || status === 'completed'\n ? 'var(--stepper-label-text)'\n : 'var(--stepper-description-text)',\n };\n\n const descriptionStyles: CSSProperties = {\n color: 'var(--stepper-description-text)',\n };\n\n // Render indicator content\n const renderIndicatorContent = () => {\n if (status === 'completed') {\n if (completedIcon) return completedIcon;\n return ;\n }\n if (variant === 'icon' && icon) return icon;\n if (variant === 'dots') return null;\n // Numbered variant\n return stepIndex >= 0 ? stepIndex + 1 : '';\n };\n\n const handleClick = () => {\n if (isClickable && onValueChange) {\n onValueChange(stepValue);\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (!isClickable) return;\n\n const stepList = e.currentTarget.closest('[role=\"tablist\"]');\n if (!stepList) return;\n\n const allSteps = Array.from(\n stepList.querySelectorAll('[role=\"tab\"]:not([aria-disabled=\"true\"])')\n ) as HTMLButtonElement[];\n const currentIdx = allSteps.indexOf(e.currentTarget as HTMLButtonElement);\n\n let nextIdx = currentIdx;\n const isHorizontal = orientation === 'horizontal';\n\n switch (e.key) {\n case isHorizontal ? 'ArrowRight' : 'ArrowDown':\n e.preventDefault();\n nextIdx = (currentIdx + 1) % allSteps.length;\n break;\n case isHorizontal ? 'ArrowLeft' : 'ArrowUp':\n e.preventDefault();\n nextIdx = currentIdx - 1 < 0 ? allSteps.length - 1 : currentIdx - 1;\n break;\n case 'Home':\n e.preventDefault();\n nextIdx = 0;\n break;\n case 'End':\n e.preventDefault();\n nextIdx = allSteps.length - 1;\n break;\n default:\n return;\n }\n\n const nextStep = allSteps[nextIdx];\n if (nextStep) {\n nextStep.focus();\n const nextValue = nextStep.getAttribute('data-value');\n if (nextValue && onValueChange) {\n onValueChange(nextValue);\n }\n }\n };\n\n // For horizontal, we need step + connector inline\n // For vertical, step is a row with connector below\n if (orientation === 'horizontal') {\n return (\n <>\n \n \n {renderIndicatorContent()}\n \n\n {label && (\n
\n \n {label}\n \n {description && (\n \n {description}\n \n )}\n
\n )}\n \n\n {/* Connector line between steps */}\n {!isLast && (\n \n )}\n \n );\n }\n\n // Vertical orientation\n return (\n
\n \n \n {renderIndicatorContent()}\n \n\n {label && (\n
\n \n {label}\n \n {description && (\n \n {description}\n \n )}\n
\n )}\n
\n\n {/* Vertical connector */}\n {!isLast && (\n \n )}\n \n );\n }\n);\n\nStepperStep.displayName = 'StepperStep';\n\n// ========================================\n// CONTENT COMPONENT\n// ========================================\n\ninterface StepperContentProps {\n /** Value of the step this content belongs to */\n value: string;\n /** Content to display when step is active */\n children: ReactNode;\n /** Optional className */\n className?: string;\n}\n\nconst StepperContent: FC = ({ value, children, className }) => {\n const { value: currentValue, orientation } = useStepperContext();\n const isActive = currentValue === value;\n\n if (!isActive) return null;\n\n return (\n \n {children}\n \n );\n};\n\n// ========================================\n// EXPORT COMPOUND COMPONENT\n// ========================================\n\nexport const StepperGlass = {\n Root: StepperRoot,\n List: StepperList,\n Step: StepperStep,\n Content: StepperContent,\n};\n\n// Also export individual components for flexibility\nexport { StepperRoot, StepperList, StepperStep, StepperContent };\n\n// Re-export types\nexport type {\n StepperRootProps,\n StepperListProps,\n StepperStepProps,\n StepperContentProps,\n StepperOrientation,\n StepperVariant,\n StepperSize,\n StepStatus,\n};\n" } ], "categories": [ "ui" ], "cssVars": { "light": { "--glass-bg": "rgba(255, 255, 255, 0.1)", "--glass-border": "rgba(255, 255, 255, 0.2)", "--blur-sm": "8px", "--blur-md": "16px", "--blur-lg": "24px" }, "dark": { "--glass-bg": "rgba(255, 255, 255, 0.05)", "--glass-border": "rgba(255, 255, 255, 0.1)", "--blur-sm": "8px", "--blur-md": "16px", "--blur-lg": "24px" } } }