{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wizard-expandable",
  "type": "registry:ui",
  "description": "Expandable multi-step wizard with optional forward validation, scrollable step rail, and Motion height transitions",
  "dependencies": [
    "@base-ui/react",
    "@radix-ui/react-use-controllable-state",
    "motion"
  ],
  "registryDependencies": [
    "badge",
    "button"
  ],
  "files": [
    {
      "path": "registry/default/ui/wizard-expandable.tsx",
      "content": "\"use client\"\n\nimport React, {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type RefObject,\n} from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"@base-ui/react/scroll-area\"\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\"\nimport {\n  AnimatePresence,\n  motion,\n  MotionConfig,\n  useReducedMotion,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\n\n/** SKILL-DESIGN: standard 3-layer surface + transition-shadow */\nconst toolbarShellClass = cn(\n  \"bg-background transition-shadow duration-200\",\n  \"shadow-[0px_0px_0px_1px_rgba(0,0,0,0.06),0px_1px_2px_-1px_rgba(0,0,0,0.06),0px_2px_4px_0px_rgba(0,0,0,0.04)]\",\n  \"dark:shadow-[0px_0px_0px_1px_rgba(255,255,255,0.06),0px_1px_2px_-1px_rgba(255,255,255,0.03),0px_2px_4px_0px_rgba(0,0,0,0.2)]\"\n)\n\n/** SKILL-DESIGN: lighter 2-layer for nested well */\nconst toolbarInnerWellClass = cn(\n  \"shadow-[0px_0px_0px_1px_rgba(0,0,0,0.06),0px_1px_2px_-1px_rgba(0,0,0,0.05)]\",\n  \"dark:shadow-[0px_0px_0px_1px_rgba(255,255,255,0.08),0px_1px_2px_-1px_rgba(255,255,255,0.04)]\"\n)\n\nconst springTransition = {\n  type: \"spring\" as const,\n  bounce: 0,\n  duration: 0.25,\n}\n\nfunction scheduleAfterLayout(fn: () => void) {\n  requestAnimationFrame(() => {\n    requestAnimationFrame(fn)\n  })\n}\n\nexport interface WizardStep {\n  id: string\n  title: string\n  description: string\n  icon:\n    | React.ComponentType<{ className?: string }>\n    | React.ReactElement<{ className?: string }>\n  content: React.ReactNode\n}\n\nexport interface WizardNavigateContext {\n  fromStepId: string | null\n  toStepId: string\n  fromIndex: number\n  toIndex: number\n}\n\ntype StepNavigationResult =\n  | { ok: true }\n  | { ok: false; context: WizardNavigateContext }\n\nfunction evaluateStepNavigation({\n  steps,\n  fromId,\n  toId,\n  canNavigateToStep,\n  skipValidationForBackward,\n}: {\n  steps: WizardStep[]\n  fromId: string | null\n  toId: string\n  canNavigateToStep: ((context: WizardNavigateContext) => boolean) | undefined\n  skipValidationForBackward: boolean\n}): StepNavigationResult {\n  if (!fromId || fromId === toId) {\n    return { ok: true }\n  }\n\n  const toIndex = steps.findIndex((s) => s.id === toId)\n  const fromIndex = steps.findIndex((s) => s.id === fromId)\n  if (toIndex < 0 || fromIndex < 0) {\n    return { ok: true }\n  }\n\n  if (!canNavigateToStep) {\n    return { ok: true }\n  }\n\n  const goingBackward = toIndex < fromIndex\n  if (skipValidationForBackward && goingBackward) {\n    return { ok: true }\n  }\n\n  const context: WizardNavigateContext = {\n    fromStepId: fromId,\n    toStepId: toId,\n    fromIndex,\n    toIndex,\n  }\n\n  if (!canNavigateToStep(context)) {\n    return { ok: false, context }\n  }\n\n  return { ok: true }\n}\n\nexport interface WizardExpandableContextValue {\n  activeStepId: string | null\n  activeIndex: number\n  totalSteps: number\n  isLastStep: boolean\n  canGoBack: boolean\n  /** False when forward navigation / complete is blocked by validation or flags. */\n  canGoNext: boolean\n  goNext: () => void\n  goBack: () => void\n  complete: () => void\n}\n\nconst WizardExpandableContext =\n  createContext<WizardExpandableContextValue | null>(null)\n\nexport function useWizardExpandable(): WizardExpandableContextValue {\n  const ctx = useContext(WizardExpandableContext)\n  if (!ctx) {\n    throw new Error(\"useWizardExpandable must be used within WizardExpandable\")\n  }\n  return ctx\n}\n\nexport interface WizardExpandableProps {\n  steps: WizardStep[]\n  badgeText?: string\n\n  className?: string\n  expanded?: boolean\n  onExpandedChange?: (expanded: boolean) => void\n  activeStep?: string | null\n  onActiveStepChange?: (stepId: string | null) => void\n\n  /**\n   * Return false to block moving from the current step to `toStepId`.\n   * Not called when opening the first step (`fromStepId` is null), closing the wizard,\n   * or when `skipValidationForBackward` is true and the target step is before the current one.\n   */\n  canNavigateToStep?: (context: WizardNavigateContext) => boolean\n  /**\n   * When true, navigating to an earlier step does not call `canNavigateToStep`.\n   * @default true\n   */\n  skipValidationForBackward?: boolean\n  /** Called when navigation was blocked by `canNavigateToStep` returning false. */\n  onNavigateBlocked?: (context: WizardNavigateContext) => void\n\n  /**\n   * When `false`, disables Next and the Complete action. When `undefined`, only\n   * `canNavigateToStep` gates forward moves (and Complete stays enabled unless you set this).\n   */\n  canGoNext?: boolean\n  /** @default true */\n  showNavigation?: boolean\n  backLabel?: string\n  nextLabel?: string\n  completeLabel?: string\n  /** Called when the user activates Complete on the last step. */\n  onComplete?: () => void\n}\n\nconst WizardExpandableInner = React.memo<WizardExpandableProps>(\n  function WizardExpandableInner({\n    steps,\n    badgeText,\n\n    className,\n    expanded: controlledExpanded,\n    onExpandedChange,\n    activeStep: controlledActiveStep,\n    onActiveStepChange,\n    canNavigateToStep,\n    skipValidationForBackward = true,\n    onNavigateBlocked,\n    canGoNext: canGoNextProp,\n    showNavigation = true,\n    backLabel = \"Back\",\n    nextLabel = \"Next\",\n    completeLabel = \"Finish\",\n    onComplete,\n  }) {\n    const reduceMotion = useReducedMotion()\n    const baseId = useId()\n    const panelId = `${baseId}-panel`\n    const tabRefs = useRef<(HTMLButtonElement | null)[]>([])\n\n    const [active, setActive] = useControllableState<string | null>({\n      prop: controlledActiveStep,\n      defaultProp: null,\n      onChange: onActiveStepChange,\n    })\n\n    const [isOpen, setIsOpen] = useControllableState({\n      prop: controlledExpanded,\n      defaultProp: false,\n      onChange: onExpandedChange,\n    })\n\n    const [hoveredTabIndex, setHoveredTabIndex] = useState<number | null>(null)\n\n    const [previousIndex, setPreviousIndex] = useState<number | null>(null)\n    const [contentRef, contentBounds] = useMeasure()\n    const [menuRef, menuBounds] = useMeasure()\n    const menuContainerRef = useRef<HTMLElement | null>(null)\n    const ref = useRef<HTMLDivElement>(null)\n    const [maxWidth, setMaxWidth] = useState(0)\n\n    const heightContent = contentBounds.height\n    const widthContainer = menuBounds.width\n\n    const scrollBehavior: ScrollBehavior = reduceMotion ? \"auto\" : \"smooth\"\n\n    const motionTransition = useMemo(\n      () => (reduceMotion ? { duration: 0.01 } : springTransition),\n      [reduceMotion]\n    )\n\n    const handleClickOutside = useCallback(() => {\n      setIsOpen(false)\n      setActive(null)\n    }, [setIsOpen, setActive])\n\n    useClickOutside(ref, handleClickOutside)\n\n    useEffect(() => {\n      if (!widthContainer || maxWidth > 0) {\n        return\n      }\n      setMaxWidth(widthContainer)\n    }, [widthContainer, maxWidth])\n\n    const scrollButtonIntoView = useCallback(\n      (\n        currentIndex: number,\n        priorIndex: number | null,\n        behavior: ScrollBehavior\n      ) => {\n        if (!menuContainerRef.current) {\n          return\n        }\n\n        const isMovingForward = priorIndex !== null && currentIndex > priorIndex\n        const isMovingBackward =\n          priorIndex !== null && currentIndex < priorIndex\n\n        let targetIndex = currentIndex\n\n        if (isMovingForward) {\n          const nextIndex = currentIndex + 1\n          if (nextIndex < steps.length) {\n            targetIndex = nextIndex\n          }\n        } else if (isMovingBackward) {\n          const prevIndex = currentIndex - 1\n          if (prevIndex >= 0) {\n            targetIndex = prevIndex\n          }\n        }\n\n        const viewport = menuContainerRef.current.querySelector(\n          '[data-slot=\"scroll-area-viewport\"]'\n        ) as HTMLElement | null\n\n        const targetButton = menuContainerRef.current.querySelector(\n          `[data-step-index=\"${targetIndex}\"]`\n        ) as HTMLElement | null\n\n        if (viewport && targetButton) {\n          scrollElementToCenterInViewport(viewport, targetButton, behavior)\n        }\n      },\n      [steps.length]\n    )\n\n    const openStep = useCallback(\n      (item: string, priorIndex: number | null) => {\n        const currentIndex = steps.findIndex((step) => step.id === item)\n        setActive(item)\n        setIsOpen(true)\n\n        if (currentIndex >= 0) {\n          scheduleAfterLayout(() => {\n            scrollButtonIntoView(currentIndex, priorIndex, scrollBehavior)\n          })\n          setPreviousIndex(currentIndex)\n        }\n      },\n      [steps, setActive, setIsOpen, scrollButtonIntoView, scrollBehavior]\n    )\n\n    const tryNavigateToStep = useCallback(\n      (item: string, priorIndex: number | null): boolean => {\n        const result = evaluateStepNavigation({\n          steps,\n          fromId: active,\n          toId: item,\n          canNavigateToStep,\n          skipValidationForBackward,\n        })\n        if (result.ok === false) {\n          onNavigateBlocked?.(result.context)\n          return false\n        }\n        openStep(item, priorIndex)\n        return true\n      },\n      [\n        active,\n        canNavigateToStep,\n        onNavigateBlocked,\n        openStep,\n        skipValidationForBackward,\n        steps,\n      ]\n    )\n\n    const activeIndex = useMemo(\n      () => (active ? steps.findIndex((s) => s.id === active) : -1),\n      [active, steps]\n    )\n\n    const isLastStep = activeIndex >= 0 && activeIndex === steps.length - 1\n    const nextStepId =\n      activeIndex >= 0 && activeIndex < steps.length - 1\n        ? steps[activeIndex + 1]?.id\n        : undefined\n    const prevStepId = activeIndex > 0 ? steps[activeIndex - 1]?.id : undefined\n\n    const forwardNavOk = useMemo(() => {\n      if (!nextStepId || active === null) {\n        return true\n      }\n      return evaluateStepNavigation({\n        steps,\n        fromId: active,\n        toId: nextStepId,\n        canNavigateToStep,\n        skipValidationForBackward,\n      }).ok\n    }, [\n      active,\n      canNavigateToStep,\n      nextStepId,\n      skipValidationForBackward,\n      steps,\n    ])\n\n    const nextDisabled = !forwardNavOk || canGoNextProp === false\n    const completeDisabled = canGoNextProp === false\n    const canGoBack = isOpen && active !== null && activeIndex > 0\n\n    const effectiveCanGoNext = isLastStep\n      ? canGoNextProp !== false\n      : forwardNavOk && canGoNextProp !== false\n\n    const handleWizardBack = useCallback(() => {\n      if (!prevStepId) {\n        return\n      }\n      const moved = tryNavigateToStep(prevStepId, previousIndex)\n      if (moved) {\n        queueMicrotask(() => tabRefs.current[activeIndex - 1]?.focus())\n      }\n    }, [activeIndex, previousIndex, prevStepId, tryNavigateToStep])\n\n    const handleWizardNext = useCallback(() => {\n      if (!nextStepId || isLastStep) {\n        return\n      }\n      const moved = tryNavigateToStep(nextStepId, previousIndex)\n      if (moved) {\n        queueMicrotask(() => tabRefs.current[activeIndex + 1]?.focus())\n      }\n    }, [activeIndex, isLastStep, nextStepId, previousIndex, tryNavigateToStep])\n\n    const handleWizardComplete = useCallback(() => {\n      if (completeDisabled) {\n        return\n      }\n      onComplete?.()\n    }, [completeDisabled, onComplete])\n\n    const handleNavClick = useCallback(\n      (item: string) => {\n        if (active === item && isOpen) {\n          setIsOpen(false)\n          setActive(null)\n          return\n        }\n\n        tryNavigateToStep(item, previousIndex)\n      },\n      [active, isOpen, previousIndex, setActive, setIsOpen, tryNavigateToStep]\n    )\n\n    useEffect(() => {\n      if (!isOpen) {\n        return\n      }\n\n      const onKeyDown = (event: KeyboardEvent) => {\n        if (event.key !== \"Escape\") {\n          return\n        }\n        const root = ref.current\n        if (!root?.contains(document.activeElement)) {\n          return\n        }\n        event.preventDefault()\n        setIsOpen(false)\n        setActive(null)\n        tabRefs.current[0]?.focus()\n      }\n\n      document.addEventListener(\"keydown\", onKeyDown)\n      return () => document.removeEventListener(\"keydown\", onKeyDown)\n    }, [isOpen, setActive, setIsOpen])\n\n    const handleTabKeyDown = useCallback(\n      (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {\n        const { key } = event\n        let nextIndex: number | undefined\n\n        if (key === \"ArrowRight\" || key === \"ArrowLeft\") {\n          event.preventDefault()\n          const delta = key === \"ArrowRight\" ? 1 : -1\n          nextIndex = (index + delta + steps.length) % steps.length\n        } else if (key === \"Home\") {\n          event.preventDefault()\n          nextIndex = 0\n        } else if (key === \"End\") {\n          event.preventDefault()\n          nextIndex = steps.length - 1\n        }\n\n        if (nextIndex === undefined) {\n          return\n        }\n\n        const nextId = steps[nextIndex]?.id\n        if (!nextId) {\n          return\n        }\n\n        const moved = tryNavigateToStep(nextId, previousIndex)\n        if (!moved) {\n          return\n        }\n\n        if (key === \"ArrowRight\" || key === \"ArrowLeft\") {\n          queueMicrotask(() => tabRefs.current[nextIndex]?.focus())\n        } else {\n          tabRefs.current[nextIndex]?.focus()\n        }\n      },\n      [previousIndex, steps, tryNavigateToStep]\n    )\n\n    const renderContent = useCallback(() => {\n      if (!active) {\n        return null\n      }\n\n      const step = steps.find((s) => s.id === active)\n      if (!step) {\n        return null\n      }\n\n      const titleId = `${baseId}-title-${step.id}`\n      const listVariants = {\n        initial: {},\n        animate: {\n          transition: {\n            staggerChildren: reduceMotion ? 0 : 0.06,\n            delayChildren: reduceMotion ? 0 : 0.02,\n          },\n        },\n        exit: {\n          transition: { staggerChildren: 0.03, staggerDirection: -1 as const },\n        },\n      }\n\n      const itemVariants = {\n        initial: reduceMotion\n          ? { opacity: 0.001 }\n          : { opacity: 0, y: 8, filter: \"blur(4px)\" },\n        animate: {\n          opacity: 1,\n          y: 0,\n          filter: \"blur(0px)\",\n          transition: {\n            duration: reduceMotion ? 0.01 : 0.22,\n            ease: [0.23, 1, 0.32, 1] as const,\n          },\n        },\n        exit: reduceMotion\n          ? {\n              opacity: 0,\n              transition: { duration: 0.1, ease: [0.23, 1, 0.32, 1] as const },\n            }\n          : {\n              opacity: 0,\n              y: -10,\n              filter: \"blur(4px)\",\n              transition: { duration: 0.16, ease: [0.4, 0, 1, 1] as const },\n            },\n      }\n\n      return (\n        <motion.div\n          animate=\"animate\"\n          aria-labelledby={`${baseId}-tab-${step.id}`}\n          className=\"space-y-4 pb-2\"\n          exit=\"exit\"\n          id={panelId}\n          initial=\"initial\"\n          key={step.id}\n          role=\"tabpanel\"\n          variants={listVariants}\n        >\n          <div className=\"space-y-2\">\n            <motion.h3\n              className=\"text-balance font-medium text-foreground text-lg\"\n              id={titleId}\n              variants={itemVariants}\n            >\n              {step.title}\n            </motion.h3>\n            <motion.p\n              className=\"text-pretty text-muted-foreground text-sm\"\n              variants={itemVariants}\n            >\n              {step.description}\n            </motion.p>\n          </div>\n          <motion.div variants={itemVariants}>{step.content}</motion.div>\n          {showNavigation ? (\n            <motion.div className=\"pt-1\" variants={itemVariants}>\n              <fieldset\n                aria-label=\"Wizard navigation\"\n                className=\"flex min-w-0 gap-2 sm:gap-3\"\n              >\n                <Button\n                  className=\"min-h-11 flex-1 sm:min-h-10\"\n                  disabled={!canGoBack}\n                  onClick={handleWizardBack}\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  {backLabel}\n                </Button>\n                {isLastStep ? (\n                  <Button\n                    className=\"min-h-11 flex-1 sm:min-h-10\"\n                    disabled={completeDisabled}\n                    onClick={handleWizardComplete}\n                    type=\"button\"\n                  >\n                    {completeLabel}\n                  </Button>\n                ) : (\n                  <Button\n                    className=\"min-h-11 flex-1 sm:min-h-10\"\n                    disabled={nextDisabled}\n                    onClick={handleWizardNext}\n                    type=\"button\"\n                  >\n                    {nextLabel}\n                  </Button>\n                )}\n              </fieldset>\n            </motion.div>\n          ) : null}\n        </motion.div>\n      )\n    }, [\n      active,\n      backLabel,\n      baseId,\n      canGoBack,\n      completeDisabled,\n      completeLabel,\n      handleWizardBack,\n      handleWizardComplete,\n      handleWizardNext,\n      isLastStep,\n      nextDisabled,\n      nextLabel,\n      panelId,\n      reduceMotion,\n      showNavigation,\n      steps,\n    ])\n\n    const wizardContextValue = useMemo<WizardExpandableContextValue>(\n      () => ({\n        activeStepId: active,\n        activeIndex,\n        totalSteps: steps.length,\n        isLastStep,\n        canGoBack,\n        canGoNext: effectiveCanGoNext,\n        goNext: handleWizardNext,\n        goBack: handleWizardBack,\n        complete: handleWizardComplete,\n      }),\n      [\n        active,\n        activeIndex,\n        canGoBack,\n        effectiveCanGoNext,\n        handleWizardBack,\n        handleWizardComplete,\n        handleWizardNext,\n        isLastStep,\n        steps.length,\n      ]\n    )\n\n    const navigationButtons = useMemo(\n      () =>\n        steps.map((step, index) => ({\n          id: step.id,\n          label: step.title,\n          step: (index + 1).toString(),\n          onClick: () => handleNavClick(step.id),\n          isActive: active === step.id,\n          isFirst: index === 0,\n          isLast: index === steps.length - 1,\n        })),\n      [steps, active, handleNavClick]\n    )\n\n    return (\n      <WizardExpandableContext.Provider value={wizardContextValue}>\n        <div\n          className={cn(\n            \"mx-auto w-full max-w-sm space-y-2 px-2 sm:max-w-lg sm:px-0\",\n            className\n          )}\n        >\n          {badgeText && (\n            <Badge\n              className=\"border-border bg-muted text-muted-foreground\"\n              variant=\"outline\"\n            >\n              {badgeText}\n            </Badge>\n          )}\n\n          <MotionConfig transition={motionTransition}>\n            <div\n              className={cn(\n                \"w-full overflow-hidden rounded-[calc(var(--radius)+var(--toolbar-gutter))] antialiased\",\n                toolbarShellClass\n              )}\n              ref={ref}\n              style={{ \"--toolbar-gutter\": \"0.5rem\" } as React.CSSProperties}\n            >\n              <div className=\"min-w-0\">\n                <div\n                  className={cn(\n                    \"relative z-10\",\n                    isOpen ? \"border-border/60 border-b\" : \"\"\n                  )}\n                >\n                  <ScrollArea\n                    className=\"w-full\"\n                    maskHeight={16}\n                    ref={(element) => {\n                      if (element) {\n                        menuContainerRef.current = element\n                        menuRef(element)\n                      }\n                    }}\n                    viewportClassName=\"scrollbar-hide\"\n                  >\n                    <div\n                      aria-label=\"Wizard steps\"\n                      className=\"flex w-max min-w-full touch-pan-x items-center p-px\"\n                      role=\"tablist\"\n                    >\n                      {navigationButtons.map((button, index) => {\n                        const tabFocusIndex =\n                          active === null ? index === 0 : button.isActive\n\n                        const isImmediatelyRightOfActive =\n                          index > 0 &&\n                          active !== null &&\n                          steps[index - 1]?.id === active\n\n                        const isImmediatelyLeftOfActive =\n                          index < steps.length - 1 &&\n                          active !== null &&\n                          steps[index + 1]?.id === active\n\n                        const activeTabFlushLeftWithHoveredNeighbor =\n                          button.isActive &&\n                          index > 0 &&\n                          hoveredTabIndex === index - 1\n\n                        const activeTabFlushRightWithHoveredNeighbor =\n                          button.isActive &&\n                          index < steps.length - 1 &&\n                          hoveredTabIndex === index + 1\n\n                        return (\n                          <button\n                            aria-controls={panelId}\n                            aria-selected={button.isActive}\n                            className={cn(\n                              \"flex min-h-[44px] shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 py-3 text-muted-foreground text-sm outline-none transition-[color,background-color,transform] duration-150 ease-out sm:min-h-0 sm:px-4 sm:py-4\",\n                              \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n                              \"active:scale-[0.98]\",\n                              button.isFirst &&\n                                \"rounded-tl-none rounded-bl-none\",\n                              button.isLast &&\n                                \"rounded-tr-none rounded-br-none\",\n                              isImmediatelyRightOfActive && \"rounded-l-none\",\n                              isImmediatelyLeftOfActive && \"rounded-r-none\",\n                              activeTabFlushLeftWithHoveredNeighbor &&\n                                \"rounded-l-none\",\n                              activeTabFlushRightWithHoveredNeighbor &&\n                                \"rounded-r-none\",\n                              button.isActive\n                                ? \"rounded-b-none bg-muted/50 font-medium text-foreground\"\n                                : \"hover:bg-muted/60 active:bg-muted/70\"\n                            )}\n                            data-step-index={index}\n                            id={`${baseId}-tab-${button.id}`}\n                            key={button.id}\n                            onClick={button.onClick}\n                            onKeyDown={(e) => handleTabKeyDown(e, index)}\n                            onMouseEnter={() => setHoveredTabIndex(index)}\n                            onMouseLeave={() => setHoveredTabIndex(null)}\n                            ref={(el) => {\n                              tabRefs.current[index] = el\n                            }}\n                            role=\"tab\"\n                            tabIndex={tabFocusIndex ? 0 : -1}\n                            type=\"button\"\n                          >\n                            <div\n                              className={cn(\n                                \"flex size-5 shrink-0 items-center justify-center rounded-full font-bold text-[10px] tabular-nums leading-none shadow-[0px_1px_1px_0px_hsla(0,0%,0%,0.02)_inset,0px_1px_1px_0px_hsla(0,0%,0%,0.02)_inset,0px_0px_0px_1px_rgba(255,255,255,0.25)] transition-colors duration-200 ease-out sm:size-5 dark:shadow-[0px_1px_1px_0px_hsla(0,0%,100%,0.02)_inset,0px_1px_1px_0px_rgba(255,255,255,0.05)_inset,0px_0px_0px_1px_hsla(0,0%,100%,0.05)_inset,0px_0px_1px_0px_rgba(0,0,0,0.25)]\",\n                                button.isActive\n                                  ? \"bg-primary/15 text-primary ring-1 ring-primary/25\"\n                                  : \"bg-muted/50 text-muted-foreground\"\n                              )}\n                            >\n                              {button.step}\n                            </div>\n                            <span className=\"font-medium text-foreground text-xs sm:text-sm\">\n                              {button.label}\n                            </span>\n                          </button>\n                        )\n                      })}\n                    </div>\n                  </ScrollArea>\n                </div>\n\n                <div className=\"overflow-hidden\">\n                  <AnimatePresence initial={false} mode=\"wait\">\n                    {isOpen && active ? (\n                      <motion.div\n                        animate={{ height: heightContent || 0 }}\n                        exit={{ height: 0 }}\n                        initial={{ height: 0 }}\n                        key=\"expanded-content\"\n                        transition={motionTransition}\n                      >\n                        <div\n                          className=\"px-(--toolbar-gutter) pt-0 pb-2\"\n                          ref={contentRef}\n                        >\n                          <div\n                            className={cn(\n                              \"rounded-t-none rounded-b-(--radius) bg-muted/50 px-3 pt-4 pb-1\",\n                              toolbarInnerWellClass\n                            )}\n                          >\n                            <AnimatePresence initial={false} mode=\"wait\">\n                              {renderContent()}\n                            </AnimatePresence>\n                          </div>\n                        </div>\n                      </motion.div>\n                    ) : null}\n                  </AnimatePresence>\n                </div>\n              </div>\n            </div>\n          </MotionConfig>\n        </div>\n      </WizardExpandableContext.Provider>\n    )\n  }\n)\n\nWizardExpandableInner.displayName = \"WizardExpandableInner\"\n\n// ________________________ HOOKS ________________________\ninterface Bounds {\n  left: number\n  top: number\n  width: number\n  height: number\n}\n\nfunction useMeasure(): [\n  (node: HTMLElement | null) => void,\n  Bounds,\n  () => void,\n] {\n  const [bounds, setBounds] = useState<Bounds>({\n    left: 0,\n    top: 0,\n    width: 0,\n    height: 0,\n  })\n\n  const [node, setNode] = useState<HTMLElement | null>(null)\n  const observer = useRef<ResizeObserver | null>(null)\n\n  const disconnect = useCallback(() => {\n    if (observer.current) {\n      observer.current.disconnect()\n    }\n  }, [])\n\n  const ref = useCallback((node: HTMLElement | null) => {\n    setNode(node)\n  }, [])\n\n  useEffect(() => {\n    if (!node) {\n      return\n    }\n\n    if (observer.current) {\n      observer.current.disconnect()\n    }\n\n    observer.current = new ResizeObserver(([entry]) => {\n      if (!entry) {\n        return\n      }\n      const borderBox = entry.borderBoxSize?.[0]\n      if (borderBox) {\n        setBounds({\n          left: 0,\n          top: 0,\n          width: borderBox.inlineSize,\n          height: borderBox.blockSize,\n        })\n        return\n      }\n      const rect = entry.contentRect\n      if (!rect) {\n        return\n      }\n      const { left, top, width, height } = rect\n      setBounds({ left, top, width, height })\n    })\n\n    observer.current.observe(node)\n\n    return () => {\n      if (observer.current) {\n        observer.current.disconnect()\n      }\n    }\n  }, [node])\n\n  return [ref, bounds, disconnect]\n}\n\nfunction useClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null>,\n  handler: (event: MouseEvent | TouchEvent) => void\n) {\n  useEffect(() => {\n    const listener = (event: MouseEvent | TouchEvent) => {\n      const el = ref?.current\n      if (!el || el.contains((event?.target as Node) || null)) {\n        return\n      }\n\n      handler(event)\n    }\n\n    document.addEventListener(\"mousedown\", listener)\n    document.addEventListener(\"touchstart\", listener)\n\n    return () => {\n      document.removeEventListener(\"mousedown\", listener)\n      document.removeEventListener(\"touchstart\", listener)\n    }\n  }, [ref, handler])\n}\n\nfunction scrollElementToCenterInViewport(\n  viewport: HTMLElement,\n  target: HTMLElement,\n  behavior: ScrollBehavior = \"smooth\"\n) {\n  const viewportRect = viewport.getBoundingClientRect()\n  const targetRect = target.getBoundingClientRect()\n  const targetCenter = targetRect.left + targetRect.width / 2\n  const viewportCenter = viewportRect.left + viewportRect.width / 2\n  const delta = targetCenter - viewportCenter\n  const maxScroll = Math.max(0, viewport.scrollWidth - viewport.clientWidth)\n  const nextLeft = Math.max(0, Math.min(maxScroll, viewport.scrollLeft + delta))\n  viewport.scrollTo({ left: nextLeft, behavior })\n}\n\nfunction useTouchPrimary() {\n  const [isTouchPrimary, setIsTouchPrimary] = useState(false)\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") {\n      return\n    }\n\n    const controller = new AbortController()\n    const { signal } = controller\n\n    const handleTouch = () => {\n      const hasTouch = \"ontouchstart\" in window || navigator.maxTouchPoints > 0\n      const prefersTouch = window.matchMedia(\"(pointer: coarse)\").matches\n      setIsTouchPrimary(hasTouch && prefersTouch)\n    }\n\n    const mq = window.matchMedia(\"(pointer: coarse)\")\n    mq.addEventListener(\"change\", handleTouch, { signal })\n    window.addEventListener(\"pointerdown\", handleTouch, { signal })\n\n    handleTouch()\n\n    return () => controller.abort()\n  }, [])\n\n  return isTouchPrimary\n}\n\n// ________________________ MODIFIED SCROLL AREA ________________________\n// https://lina.sameer.sh/\n\nconst ScrollAreaContext = React.createContext<boolean>(false)\n\ninterface ScrollFadeMask {\n  top: boolean\n  bottom: boolean\n  left: boolean\n  right: boolean\n}\n\nconst ScrollArea = React.forwardRef<\n  React.ComponentRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {\n    viewportClassName?: string\n    /**\n     * `maskHeight` is the height of the mask in pixels.\n     * pass `0` to disable the mask\n     * @default 30\n     */\n    maskHeight?: number\n    maskClassName?: string\n  }\n>(\n  (\n    {\n      className,\n      children,\n      viewportClassName,\n      maskClassName,\n      maskHeight = 30,\n      ...props\n    },\n    ref\n  ) => {\n    const [showMask, setShowMask] = React.useState<ScrollFadeMask>({\n      top: false,\n      bottom: false,\n      left: false,\n      right: false,\n    })\n    const viewportRef = React.useRef<HTMLDivElement>(null)\n    const isTouch = useTouchPrimary()\n\n    const checkScrollability = React.useCallback(() => {\n      const element = viewportRef.current\n      if (!element) {\n        return\n      }\n\n      const {\n        scrollTop,\n        scrollLeft,\n        scrollWidth,\n        clientWidth,\n        scrollHeight,\n        clientHeight,\n      } = element\n      setShowMask((prev) => ({\n        ...prev,\n        top: scrollTop > 0,\n        bottom: scrollTop + clientHeight < scrollHeight - 1,\n        left: scrollLeft > 0,\n        right: scrollLeft + clientWidth < scrollWidth - 1,\n      }))\n    }, [])\n\n    React.useEffect(() => {\n      if (typeof window === \"undefined\") {\n        return\n      }\n\n      const element = viewportRef.current\n      if (!element) {\n        return\n      }\n\n      const controller = new AbortController()\n      const { signal } = controller\n\n      const resizeObserver = new ResizeObserver(checkScrollability)\n      resizeObserver.observe(element)\n\n      element.addEventListener(\"scroll\", checkScrollability, { signal })\n      window.addEventListener(\"resize\", checkScrollability, { signal })\n\n      checkScrollability()\n\n      return () => {\n        controller.abort()\n        resizeObserver.disconnect()\n      }\n    }, [checkScrollability])\n\n    return (\n      <ScrollAreaContext.Provider value={isTouch}>\n        {isTouch ? (\n          <section\n            aria-label=\"Scrollable area\"\n            aria-roledescription=\"scroll area\"\n            className={cn(\"relative overflow-hidden\", className)}\n            data-slot=\"scroll-area\"\n            ref={ref}\n            {...(props as React.HTMLAttributes<HTMLElement>)}\n          >\n            <div\n              className={cn(\n                \"size-full overflow-auto overscroll-x-contain rounded-[inherit]\",\n                viewportClassName\n              )}\n              data-slot=\"scroll-area-viewport\"\n              ref={viewportRef}\n            >\n              {children}\n            </div>\n\n            {maskHeight > 0 && (\n              <ScrollMask\n                className={maskClassName}\n                maskHeight={maskHeight}\n                showMask={showMask}\n              />\n            )}\n          </section>\n        ) : (\n          <ScrollAreaPrimitive.Root\n            className={cn(\"relative overflow-hidden\", className)}\n            data-slot=\"scroll-area\"\n            ref={ref}\n            {...props}\n          >\n            <ScrollAreaPrimitive.Viewport\n              className={cn(\n                \"size-full overscroll-x-contain rounded-[inherit]\",\n                viewportClassName\n              )}\n              data-slot=\"scroll-area-viewport\"\n              ref={viewportRef}\n            >\n              {children}\n            </ScrollAreaPrimitive.Viewport>\n\n            {maskHeight > 0 && (\n              <ScrollMask\n                className={maskClassName}\n                maskHeight={maskHeight}\n                showMask={showMask}\n              />\n            )}\n            <ScrollBar />\n            <ScrollAreaPrimitive.Corner />\n          </ScrollAreaPrimitive.Root>\n        )}\n      </ScrollAreaContext.Provider>\n    )\n  }\n)\n\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName\n\nconst ScrollBar = React.forwardRef<\n  React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => {\n  const isTouch = React.useContext(ScrollAreaContext)\n\n  if (isTouch) {\n    return null\n  }\n\n  return (\n    <ScrollAreaPrimitive.Scrollbar\n      className={cn(\n        \"data-[state=visible]:fade-in-0 data-[state=hidden]:fade-out-0 flex touch-none select-none p-px transition-[colors] duration-150 hover:bg-muted data-[state=hidden]:animate-out data-[state=visible]:animate-in dark:hover:bg-muted/50\",\n        orientation === \"vertical\" &&\n          \"h-full w-2.5 border-l border-l-transparent\",\n        orientation === \"horizontal\" &&\n          \"h-2.5 flex-col border-t border-t-transparent px-1 pr-1.25\",\n        className\n      )}\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      ref={ref}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Thumb\n        className={cn(\n          \"relative flex-1 origin-center rounded-full bg-border transition-[scale]\",\n          orientation === \"vertical\" && \"my-1 active:scale-y-95\",\n          orientation === \"horizontal\" && \"active:scale-x-98\"\n        )}\n        data-slot=\"scroll-area-thumb\"\n      />\n    </ScrollAreaPrimitive.Scrollbar>\n  )\n})\n\nScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName\n\nconst ScrollMask = ({\n  showMask,\n  maskHeight,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showMask: ScrollFadeMask\n  maskHeight: number\n}) => {\n  return (\n    <>\n      <div\n        {...props}\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10\",\n          \"before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']\",\n          \"after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']\",\n          \"before:h-(--top-fade-height) after:h-(--bottom-fade-height)\",\n          showMask.top ? \"before:opacity-100\" : \"before:opacity-0\",\n          showMask.bottom ? \"after:opacity-100\" : \"after:opacity-0\",\n          \"before:bg-linear-to-b before:from-background before:to-transparent\",\n          \"after:bg-linear-to-t after:from-background after:to-transparent\",\n          className\n        )}\n        style={\n          {\n            \"--top-fade-height\": showMask.top ? `${maskHeight}px` : \"0px\",\n            \"--bottom-fade-height\": showMask.bottom ? `${maskHeight}px` : \"0px\",\n          } as React.CSSProperties\n        }\n      />\n      <div\n        {...props}\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10\",\n          \"before:absolute before:inset-y-0 before:left-0 before:transition-[width,opacity] before:duration-300 before:content-['']\",\n          \"after:absolute after:inset-y-0 after:right-0 after:transition-[width,opacity] after:duration-300 after:content-['']\",\n          \"before:w-(--left-fade-width) after:w-(--right-fade-width)\",\n          showMask.left ? \"before:opacity-100\" : \"before:opacity-0\",\n          showMask.right ? \"after:opacity-100\" : \"after:opacity-0\",\n          \"before:bg-linear-to-r before:from-background before:to-transparent\",\n          \"after:bg-linear-to-l after:from-background after:to-transparent\",\n          className\n        )}\n        style={\n          {\n            \"--left-fade-width\": showMask.left ? `${maskHeight}px` : \"0px\",\n            \"--right-fade-width\": showMask.right ? `${maskHeight}px` : \"0px\",\n          } as React.CSSProperties\n        }\n      />\n    </>\n  )\n}\n\nexport default function WizardExpandable(props: WizardExpandableProps) {\n  return <WizardExpandableInner {...props} />\n}\n",
      "type": "registry:ui"
    }
  ]
}