{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toolbar-expandable",
  "type": "registry:ui",
  "description": "Expandable toolbar component with step-based navigation, smooth animations, and enhanced scrolling",
  "dependencies": [
    "motion",
    "@radix-ui/react-scroll-area"
  ],
  "registryDependencies": [
    "badge"
  ],
  "files": [
    {
      "path": "registry/default/ui/toolbar-expandable.tsx",
      "content": "\"use client\"\n\nimport React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type RefObject,\n} from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\nimport { AnimatePresence, motion, MotionConfig } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\n\nconst transition = {\n  type: \"spring\" as const,\n  bounce: 0.1,\n  duration: 0.25,\n}\n\ninterface DynamicStep {\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\ninterface DynamicToolbarExpandableProps {\n  steps: DynamicStep[]\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\nconst DynamicToolbarExpandable = React.memo<DynamicToolbarExpandableProps>(\n  function DynamicToolbarExpandable({\n    steps,\n    badgeText,\n\n    className,\n    expanded: controlledExpanded,\n    onExpandedChange,\n    activeStep: controlledActiveStep,\n    onActiveStepChange,\n  }) {\n    const [internalActive, setInternalActive] = useState<string | null>(null)\n    const [internalIsOpen, setInternalIsOpen] = useState(false)\n\n    const active =\n      controlledActiveStep !== undefined ? controlledActiveStep : internalActive\n    const isOpen =\n      controlledExpanded !== undefined ? controlledExpanded : internalIsOpen\n\n    const setActive = useCallback(\n      (value: string | null) => {\n        if (onActiveStepChange) {\n          onActiveStepChange(value)\n        } else {\n          setInternalActive(value)\n        }\n      },\n      [onActiveStepChange]\n    )\n\n    const setIsOpen = useCallback(\n      (value: boolean) => {\n        if (onExpandedChange) {\n          onExpandedChange(value)\n        } else {\n          setInternalIsOpen(value)\n        }\n      },\n      [onExpandedChange]\n    )\n\n    const [previousIndex, setPreviousIndex] = useState<number | null>(null)\n    const [contentRef, contentBounds] = useMeasure()\n    const [menuRef, menuBounds] = useMeasure()\n    const menuContainerRef = useRef<any>(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 handleClickOutside = useCallback(() => {\n      setIsOpen(false)\n      setActive(null)\n    }, [setIsOpen, setActive])\n\n    useClickOutside(ref, handleClickOutside)\n\n    useEffect(() => {\n      if (!widthContainer || maxWidth > 0) return\n      setMaxWidth(widthContainer)\n    }, [widthContainer, maxWidth])\n\n    const scrollButtonIntoView = useCallback(\n      (currentIndex: number, previousIndex: number | null) => {\n        if (!menuContainerRef.current) return\n\n        const isMovingForward =\n          previousIndex !== null && currentIndex > previousIndex\n        const isMovingBackward =\n          previousIndex !== null && currentIndex < previousIndex\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 targetButton = menuContainerRef.current.querySelector(\n          `[data-step-index=\"${targetIndex}\"]`\n        ) as HTMLElement\n\n        if (targetButton) {\n          targetButton.scrollIntoView({\n            behavior: \"smooth\",\n            block: \"nearest\",\n            inline: \"center\",\n          })\n        }\n      },\n      [steps.length]\n    )\n\n    const handleNavClick = useCallback(\n      (item: string) => {\n        if (active === item && isOpen) {\n          setIsOpen(false)\n          setActive(null)\n          return\n        }\n\n        const currentIndex = steps.findIndex((step) => step.id === item)\n        setActive(item)\n        setIsOpen(true)\n\n        if (currentIndex >= 0) {\n          setTimeout(() => {\n            scrollButtonIntoView(currentIndex, previousIndex)\n          }, 100)\n\n          setPreviousIndex(currentIndex)\n        }\n      },\n      [\n        active,\n        isOpen,\n        steps,\n        scrollButtonIntoView,\n        previousIndex,\n        setActive,\n        setIsOpen,\n      ]\n    )\n\n    const renderContent = useCallback(() => {\n      if (!active) return null\n\n      const step = steps.find((s) => s.id === active)\n      if (!step) return null\n\n      return (\n        <div className=\"space-y-4 pb-3\">\n          <div className=\"space-y-2\">\n            <h3 className=\"text-foreground text-lg font-medium\">\n              {step.title}\n            </h3>\n            <p className=\"text-muted-foreground text-sm\">{step.description}</p>\n          </div>\n          {step.content}\n        </div>\n      )\n    }, [active, steps])\n\n    const activeTitle = useMemo(() => {\n      if (!active) return \"\"\n      const step = steps.find((s) => s.id === active)\n      return step?.title || \"\"\n    }, [active, steps])\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      <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            variant=\"outline\"\n            className=\"bg-muted border-border text-muted-foreground\"\n          >\n            {badgeText}\n          </Badge>\n        )}\n\n        <MotionConfig transition={transition}>\n          <div\n            className=\"bg-background w-full overflow-hidden rounded-2xl shadow-[0_0_0_1px_rgba(0,0,0,0.08),0px_2px_2px_rgba(0,0,0,0.04),0px_8px_16px_-4px_rgba(0,0,0,0.04)] dark:shadow-[0_0_0_1px_rgba(255,252,240,0.08),0px_2px_2px_rgba(0,0,0,0.2),0px_8px_16px_-4px_rgba(0,0,0,0.3)]\"\n            ref={ref}\n          >\n            <div className=\"rounded-2xl\">\n              <div className=\"overflow-hidden\">\n                <AnimatePresence initial={false} mode=\"wait\">\n                  {isOpen ? (\n                    <motion.div\n                      key=\"expanded-content\"\n                      initial={{ height: 0 }}\n                      animate={{ height: heightContent || 0 }}\n                      exit={{ height: 0 }}\n                      className=\"\"\n                    >\n                      <div ref={contentRef} className=\"px-2 pt-2 sm:px-2\">\n                        <h4 className=\"text-foreground px-2 text-sm font-medium\">\n                          {activeTitle}\n                        </h4>\n\n                        <div className=\"pb-1\">\n                          <div className=\"bg-muted/50 mt-3 mb-2 rounded-[0.8rem] px-3 py-4 shadow-[0px_1px_1px_0px_rgba(0,0,0,0.05),0px_1px_1px_0px_rgba(255,252,240,0.5)_inset,0px_0px_0px_1px_hsla(0,0%,100%,0.1)_inset,0px_0px_1px_0px_rgba(28,27,26,0.5)] dark:shadow-[0px_1px_1px_0px_rgba(0,0,0,0.2),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.8)]\">\n                            {renderContent()}\n                          </div>\n                        </div>\n                      </div>\n                    </motion.div>\n                  ) : null}\n                </AnimatePresence>\n              </div>\n\n              {/* Navigation bar */}\n              <div className=\"relative z-10\">\n                <ScrollArea\n                  className=\"w-full\"\n                  viewportClassName=\"scrollbar-hide\"\n                  maskHeight={16}\n                  ref={(element) => {\n                    if (element) {\n                      menuContainerRef.current = element\n                      menuRef(element)\n                    }\n                  }}\n                >\n                  <div className=\"flex w-max min-w-full items-center p-[1px]\">\n                    {navigationButtons.map((button, index) => (\n                      <button\n                        key={button.id}\n                        data-step-index={index}\n                        onClick={button.onClick}\n                        className={cn(\n                          \"text-muted-foreground flex min-h-[44px] shrink-0 items-center gap-2 px-3 py-3 text-sm whitespace-nowrap transition-colors sm:min-h-0 sm:px-4 sm:py-4\",\n                          button.isActive\n                            ? \"text-foreground bg-muted/50 font-medium\"\n                            : \"hover:bg-muted/60 active:bg-muted/70\"\n                        )}\n                      >\n                        <div className=\"flex h-5 w-5 items-center justify-center rounded-md text-[10px] font-bold 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-all duration-300 sm:h-5 sm:w-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                          <span\n                            className={cn(\n                              \"w-full rounded-md\",\n                              button.isActive\n                                ? \"bg-blue-300/20 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400\"\n                                : \"bg-muted/50 text-muted-foreground\"\n                            )}\n                          >\n                            {button.step}\n                          </span>\n                        </div>\n                        <span className=\"text-foreground text-xs font-medium sm:text-sm\">\n                          {button.label}\n                        </span>\n                      </button>\n                    ))}\n                  </div>\n                </ScrollArea>\n              </div>\n            </div>\n          </div>\n        </MotionConfig>\n      </div>\n    )\n  }\n)\n\nDynamicToolbarExpandable.displayName = \"DynamicToolbarExpandable\"\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) return\n\n    if (observer.current) {\n      observer.current.disconnect()\n    }\n\n    observer.current = new ResizeObserver(([entry]) => {\n      if (entry && entry.contentRect) {\n        const { left, top, width, height } = entry.contentRect\n        setBounds({ left, top, width, height })\n      }\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 useTouchPrimary() {\n  const [isTouchPrimary, setIsTouchPrimary] = useState(false)\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return\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)\ntype Mask = {\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      scrollHideDelay = 0,\n      viewportClassName,\n      maskClassName,\n      maskHeight = 30,\n      ...props\n    },\n    ref\n  ) => {\n    const [showMask, setShowMask] = React.useState<Mask>({\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) return\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\") return\n\n      const element = viewportRef.current\n      if (!element) return\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, isTouch])\n\n    return (\n      <ScrollAreaContext.Provider value={isTouch}>\n        {isTouch ? (\n          <div\n            ref={ref}\n            role=\"group\"\n            data-slot=\"scroll-area\"\n            aria-roledescription=\"scroll area\"\n            className={cn(\"relative overflow-hidden\", className)}\n            {...props}\n          >\n            <div\n              ref={viewportRef}\n              data-slot=\"scroll-area-viewport\"\n              className={cn(\n                \"size-full overflow-auto rounded-[inherit]\",\n                viewportClassName\n              )}\n              tabIndex={0}\n            >\n              {children}\n            </div>\n\n            {maskHeight > 0 && (\n              <ScrollMask\n                showMask={showMask}\n                className={maskClassName}\n                maskHeight={maskHeight}\n              />\n            )}\n          </div>\n        ) : (\n          <ScrollAreaPrimitive.Root\n            ref={ref}\n            data-slot=\"scroll-area\"\n            scrollHideDelay={scrollHideDelay}\n            className={cn(\"relative overflow-hidden\", className)}\n            {...props}\n          >\n            <ScrollAreaPrimitive.Viewport\n              ref={viewportRef}\n              data-slot=\"scroll-area-viewport\"\n              className={cn(\"size-full rounded-[inherit]\", viewportClassName)}\n            >\n              {children}\n            </ScrollAreaPrimitive.Viewport>\n\n            {maskHeight > 0 && (\n              <ScrollMask\n                showMask={showMask}\n                className={maskClassName}\n                maskHeight={maskHeight}\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.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => {\n  const isTouch = React.useContext(ScrollAreaContext)\n\n  if (isTouch) return null\n\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      ref={ref}\n      orientation={orientation}\n      data-slot=\"scroll-area-scrollbar\"\n      className={cn(\n        \"hover:bg-muted dark:hover:bg-muted/50 data-[state=visible]:fade-in-0 data-[state=hidden]:fade-out-0 data-[state=visible]:animate-in data-[state=hidden]:animate-out flex touch-none p-px transition-[colors] duration-150 select-none\",\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      {...props}\n    >\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className={cn(\n          \"bg-border relative flex-1 origin-center rounded-full transition-[scale]\",\n          orientation === \"vertical\" && \"my-1 active:scale-y-95\",\n          orientation === \"horizontal\" && \"active:scale-x-98\"\n        )}\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  )\n})\n\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName\n\nconst ScrollMask = ({\n  showMask,\n  maskHeight,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showMask: Mask\n  maskHeight: number\n}) => {\n  return (\n    <>\n      <div\n        {...props}\n        aria-hidden=\"true\"\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        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:from-background before:bg-gradient-to-b before:to-transparent\",\n          \"after:from-background after:bg-gradient-to-t after:to-transparent\",\n          className\n        )}\n      />\n      <div\n        {...props}\n        aria-hidden=\"true\"\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        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:from-background before:bg-gradient-to-r before:to-transparent\",\n          \"after:from-background after:bg-gradient-to-l after:to-transparent\",\n          className\n        )}\n      />\n    </>\n  )\n}\n\nexport default function ToolbarExpandable(\n  props: DynamicToolbarExpandableProps\n) {\n  return <DynamicToolbarExpandable {...props} />\n}\n",
      "type": "registry:ui"
    }
  ]
}