# Toolbar Expandable

> Expandable toolbar for React and shadcn/ui that opens into a scrollable step-by-step panel, for onboarding and feature tours.

Source: https://www.cult-ui.com/docs/components/toolbar-expandable

## Example

```tsx title="toolbar-expandable-demo.tsx"
"use client"

import { useState } from "react"
import {
  CheckCircle,
  Code,
  Database,
  Download,
  Palette,
  Rocket,
  Settings,
  Upload,
} from "lucide-react"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import ToolbarExpandable from "@/components/ui/toolbar-expandable"

const deploymentSteps = [
  {
    id: "setup",
    title: "Project Setup",
    description:
      "Initialize your project with the required dependencies and configuration.",
    icon: Settings,
    content: (
      <div className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="project-name">Project Name</Label>
          <Input id="project-name" placeholder="my-awesome-app" />
        </div>
        <div className="space-y-2">
          <Label htmlFor="framework">Framework</Label>
          <select className="w-full rounded-md border p-2">
            <option>Next.js</option>
            <option>React</option>
            <option>Vue.js</option>
          </select>
        </div>
        <Button className="w-full">
          <Code className="mr-2 h-4 w-4" />
          Initialize Project
        </Button>
      </div>
    ),
  },
  {
    id: "configure",
    title: "Configuration",
    description:
      "Set up environment variables and project settings for optimal performance.",
    icon: Database,
    content: (
      <div className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="api-key">API Key</Label>
          <Input
            id="api-key"
            type="password"
            placeholder="Enter your API key"
          />
        </div>
        <div className="space-y-2">
          <Label htmlFor="database-url">Database URL</Label>
          <Input id="database-url" placeholder="postgresql://..." />
        </div>
        <div className="space-y-2">
          <Label htmlFor="environment">Environment</Label>
          <select className="w-full rounded-md border p-2">
            <option>Development</option>
            <option>Staging</option>
            <option>Production</option>
          </select>
        </div>
        <Button variant="outline" className="w-full bg-transparent">
          <Settings className="mr-2 h-4 w-4" />
          Save Configuration
        </Button>
      </div>
    ),
  },
  {
    id: "customize",
    title: "Customize Design",
    description:
      "Personalize your application's appearance and branding elements.",
    icon: Palette,
    content: (
      <div className="space-y-4">
        <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
          <div className="space-y-2">
            <Label>Primary Color</Label>
            <div className="flex gap-2">
              <div className="h-8 w-8 rounded-md border-2 border-blue-600 bg-blue-500"></div>
              <div className="h-8 w-8 rounded-md bg-green-500"></div>
              <div className="h-8 w-8 rounded-md bg-purple-500"></div>
            </div>
          </div>
          <div className="space-y-2">
            <Label>Theme</Label>
            <select className="w-full rounded-md border p-2">
              <option>Light</option>
              <option>Dark</option>
              <option>Auto</option>
            </select>
          </div>
        </div>
        <div className="space-y-2">
          <Label htmlFor="logo">Logo Upload</Label>
          <Input id="logo" type="file" accept="image/*" />
        </div>
        <Button variant="outline" className="w-full bg-transparent">
          <Palette className="mr-2 h-4 w-4" />
          Apply Theme
        </Button>
      </div>
    ),
  },
  {
    id: "upload",
    title: "Upload Assets",
    description:
      "Upload your project files, images, and other assets to the platform.",
    icon: Upload,
    content: (
      <div className="space-y-4">
        <div className="rounded-lg border-2 border-dashed border-gray-300 p-6 text-center">
          <Upload className="mx-auto mb-2 h-8 w-8 text-gray-400" />
          <p className="mb-2 text-sm text-gray-600">Drag and drop files here</p>
          <Button variant="outline" size="sm">
            Choose Files
          </Button>
        </div>
        <div className="space-y-2">
          <Label htmlFor="description">Asset Description</Label>
          <Textarea
            id="description"
            placeholder="Describe your assets..."
            rows={3}
          />
        </div>
        <Button className="w-full">
          <Upload className="mr-2 h-4 w-4" />
          Upload Assets
        </Button>
      </div>
    ),
  },
  {
    id: "deploy",
    title: "Deploy",
    description:
      "Deploy your application to production with automatic scaling and monitoring.",
    icon: Rocket,
    content: (
      <div className="space-y-4">
        <div className="rounded-lg border border-green-200 bg-green-50 p-4">
          <div className="mb-2 flex items-center gap-2">
            <CheckCircle className="h-5 w-5 text-green-600" />
            <span className="font-medium text-green-800">Ready to Deploy</span>
          </div>
          <p className="text-sm text-green-700">
            All checks passed. Your application is ready for production
            deployment.
          </p>
        </div>
        <div className="space-y-2">
          <Label>Deployment Region</Label>
          <select className="w-full rounded-md border p-2">
            <option>US East (Virginia)</option>
            <option>US West (California)</option>
            <option>Europe (Frankfurt)</option>
            <option>Asia Pacific (Singapore)</option>
          </select>
        </div>
        <Button className="w-full bg-green-600 hover:bg-green-700">
          <Rocket className="mr-2 h-4 w-4" />
          Deploy to Production
        </Button>
      </div>
    ),
  },
]

const downloadSteps = [
  {
    id: "format",
    title: "Choose Format",
    description: "Select the download format that best suits your needs.",
    icon: Download,
    content: (
      <div className="space-y-4">
        <div className="grid gap-3">
          <label className="flex cursor-pointer items-center gap-3 rounded-lg border p-3 hover:bg-gray-50">
            <input type="radio" name="format" value="zip" defaultChecked />
            <div>
              <div className="font-medium">ZIP Archive</div>
              <div className="text-sm text-gray-600">
                Complete project as compressed file
              </div>
            </div>
          </label>
          <label className="flex cursor-pointer items-center gap-3 rounded-lg border p-3 hover:bg-gray-50">
            <input type="radio" name="format" value="git" />
            <div>
              <div className="font-medium">Git Repository</div>
              <div className="text-sm text-gray-600">
                Clone-ready repository with history
              </div>
            </div>
          </label>
        </div>
      </div>
    ),
  },
  {
    id: "options",
    title: "Download Options",
    description: "Configure additional options for your download.",
    icon: Settings,
    content: (
      <div className="space-y-4">
        <div className="space-y-3">
          <label className="flex items-center gap-2">
            <input type="checkbox" defaultChecked />
            <span className="text-sm">Include dependencies</span>
          </label>
          <label className="flex items-center gap-2">
            <input type="checkbox" defaultChecked />
            <span className="text-sm">Include documentation</span>
          </label>
          <label className="flex items-center gap-2">
            <input type="checkbox" />
            <span className="text-sm">Include example files</span>
          </label>
        </div>
        <Button variant="outline" className="w-full bg-transparent">
          <Settings className="mr-2 h-4 w-4" />
          Configure Options
        </Button>
      </div>
    ),
  },
]

export default function DynamicToolbarDemo() {
  const [controlledExpanded, setControlledExpanded] = useState(false)
  const [controlledActiveStep, setControlledActiveStep] = useState<
    string | null
  >(null)

  return (
    <div className="">
      <div className="mx-auto max-w-sm sm:max-w-5xl">
        <div className="relative">
          <div className="px-4 pt-8 pb-8 text-center sm:px-6 sm:pt-16 sm:pb-12">
            <div className="bg-muted text-muted-foreground mb-6 inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium">
              Component Demo
            </div>
            <h1 className="text-foreground mb-3 text-2xl font-semibold text-balance sm:text-3xl">
              Dynamic Toolbar Expandable
            </h1>
            <p className="text-muted-foreground mx-auto max-w-lg text-sm leading-relaxed text-balance sm:text-base">
              Sophisticated expandable toolbar with smooth animations and
              step-based navigation for complex workflows.
            </p>
          </div>
        </div>

        <div className="mx-auto max-w-2xl space-y-12 px-4 sm:space-y-16 sm:px-0">
          <section className="sm:bg-muted/50 rounded-xl p-4 pb-6 sm:border sm:p-6 sm:pb-8">
            <div className="mb-8">
              <h2 className="text-foreground mb-2 text-lg font-medium">
                Controlled Usage
              </h2>
              <p className="text-muted-foreground text-sm">
                External state management for programmatic control
              </p>
            </div>

            <div className="mb-6 p-3 sm:p-4">
              <div className="mb-3 flex flex-wrap gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  className="text-xs sm:text-sm"
                  onClick={() => {
                    setControlledExpanded(!controlledExpanded)
                    if (!controlledExpanded && !controlledActiveStep) {
                      setControlledActiveStep("setup")
                    }
                  }}
                >
                  {controlledExpanded ? "Collapse" : "Expand"}
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  className="text-xs sm:text-sm"
                  onClick={() => {
                    setControlledExpanded(true)
                    setControlledActiveStep("configure")
                  }}
                >
                  Configuration
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  className="text-xs sm:text-sm"
                  onClick={() => {
                    setControlledExpanded(true)
                    setControlledActiveStep("deploy")
                  }}
                >
                  Deploy
                </Button>
                <Button
                  variant="ghost"
                  size="sm"
                  className="text-xs sm:text-sm"
                  onClick={() => {
                    setControlledExpanded(false)
                    setControlledActiveStep(null)
                  }}
                >
                  Reset
                </Button>
              </div>
              <div className="text-muted-foreground font-mono text-xs">
                expanded: {controlledExpanded.toString()} • active:{" "}
                {controlledActiveStep || "null"}
              </div>
            </div>

            <ToolbarExpandable
              steps={deploymentSteps}
              badgeText="CONTROLLED"
              expanded={controlledExpanded}
              onExpandedChange={setControlledExpanded}
              activeStep={controlledActiveStep}
              onActiveStepChange={setControlledActiveStep}
            />
          </section>

          <section className="sm:bg-muted/50 rounded-xl p-4 pb-6 sm:border sm:p-6 sm:pb-8">
            <div className="mb-8">
              <h2 className="text-foreground mb-2 text-lg font-medium">
                Deployment Workflow
              </h2>
              <p className="text-muted-foreground text-sm">
                5-step process with internal state management
              </p>
            </div>

            <ToolbarExpandable steps={deploymentSteps} badgeText="DEPLOYMENT" />
          </section>

          <section className="sm:bg-muted/50 rounded-xl p-4 pb-6 sm:border sm:p-6 sm:pb-8">
            <div className="mb-8">
              <h2 className="text-foreground mb-2 text-lg font-medium">
                Download Configuration
              </h2>
              <p className="text-muted-foreground text-sm">
                2-step process for download preferences
              </p>
            </div>

            <ToolbarExpandable steps={downloadSteps} badgeText="DOWNLOAD" />
          </section>

          <section className="pb-16">
            <div className="mb-8">
              <h2 className="text-foreground mb-2 text-lg font-medium">
                Features
              </h2>
              <p className="text-muted-foreground text-sm">
                Key capabilities and technical highlights
              </p>
            </div>

            <div className="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2 sm:gap-x-8">
              <div className="flex items-start gap-3">
                <div className="mt-2 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500"></div>
                <div>
                  <div className="text-foreground text-sm font-medium">
                    Smooth Animations
                  </div>
                  <div className="text-muted-foreground text-xs">
                    Framer Motion with spring transitions
                  </div>
                </div>
              </div>
              <div className="flex items-start gap-3">
                <div className="mt-2 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500"></div>
                <div>
                  <div className="text-foreground text-sm font-medium">
                    Responsive Design
                  </div>
                  <div className="text-muted-foreground text-xs">
                    Fully responsive with mobile optimization
                  </div>
                </div>
              </div>
              <div className="flex items-start gap-3">
                <div className="mt-2 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500"></div>
                <div>
                  <div className="text-foreground text-sm font-medium">
                    Enhanced Scrolling
                  </div>
                  <div className="text-muted-foreground text-xs">
                    Automatic fade masks and touch support
                  </div>
                </div>
              </div>
              <div className="flex items-start gap-3">
                <div className="mt-2 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500"></div>
                <div>
                  <div className="text-foreground text-sm font-medium">
                    Performance Optimized
                  </div>
                  <div className="text-muted-foreground text-xs">
                    Memoized components and callbacks
                  </div>
                </div>
              </div>
              <div className="flex items-start gap-3">
                <div className="mt-2 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500"></div>
                <div>
                  <div className="text-foreground text-sm font-medium">
                    Controllable State
                  </div>
                  <div className="text-muted-foreground text-xs">
                    Controlled and uncontrolled patterns
                  </div>
                </div>
              </div>
              <div className="flex items-start gap-3">
                <div className="mt-2 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-emerald-500"></div>
                <div>
                  <div className="text-foreground text-sm font-medium">
                    TypeScript Support
                  </div>
                  <div className="text-muted-foreground text-xs">
                    Fully typed with comprehensive interfaces
                  </div>
                </div>
              </div>
            </div>
          </section>
        </div>
      </div>
    </div>
  )
}
```

Toolbar Expandable is a React toolbar component for shadcn/ui built on Motion and Radix Scroll Area. Use it for onboarding flows, feature tours, or compact settings menus that open from a single bar.

## References

<Citations>
  <CitationTitle>Inspiration</CitationTitle>
  <CitationList>
    <CitationItem>
      <CitationLink
        href="https://motion-primitives.com/docs/toolbar-expandable"
        target="_blank"
        rel="noopener noreferrer"
      >
        Toolbar Expandable by Motion Primitives
      </CitationLink>
    </CitationItem>
  </CitationList>
</Citations>

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/toolbar-expandable
```

### Manual

**Copy and paste the following code into your project.**

```tsx title="toolbar-expandable.tsx"
"use client"

import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type RefObject,
} from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { AnimatePresence, motion, MotionConfig } from "motion/react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"

const transition = {
  type: "spring" as const,
  bounce: 0.1,
  duration: 0.25,
}

interface DynamicStep {
  id: string
  title: string
  description: string
  icon:
    | React.ComponentType<{ className?: string }>
    | React.ReactElement<{ className?: string }>
  content: React.ReactNode
}

interface DynamicToolbarExpandableProps {
  steps: DynamicStep[]
  badgeText?: string

  className?: string
  expanded?: boolean
  onExpandedChange?: (expanded: boolean) => void
  activeStep?: string | null
  onActiveStepChange?: (stepId: string | null) => void
}

const DynamicToolbarExpandable = React.memo<DynamicToolbarExpandableProps>(
  function DynamicToolbarExpandable({
    steps,
    badgeText,

    className,
    expanded: controlledExpanded,
    onExpandedChange,
    activeStep: controlledActiveStep,
    onActiveStepChange,
  }) {
    const [internalActive, setInternalActive] = useState<string | null>(null)
    const [internalIsOpen, setInternalIsOpen] = useState(false)

    const active =
      controlledActiveStep !== undefined ? controlledActiveStep : internalActive
    const isOpen =
      controlledExpanded !== undefined ? controlledExpanded : internalIsOpen

    const setActive = useCallback(
      (value: string | null) => {
        if (onActiveStepChange) {
          onActiveStepChange(value)
        } else {
          setInternalActive(value)
        }
      },
      [onActiveStepChange]
    )

    const setIsOpen = useCallback(
      (value: boolean) => {
        if (onExpandedChange) {
          onExpandedChange(value)
        } else {
          setInternalIsOpen(value)
        }
      },
      [onExpandedChange]
    )

    const [previousIndex, setPreviousIndex] = useState<number | null>(null)
    const [contentRef, contentBounds] = useMeasure()
    const [menuRef, menuBounds] = useMeasure()
    const menuContainerRef = useRef<any>(null)
    const ref = useRef<HTMLDivElement>(null)
    const [maxWidth, setMaxWidth] = useState(0)

    const heightContent = contentBounds.height
    const widthContainer = menuBounds.width

    const handleClickOutside = useCallback(() => {
      setIsOpen(false)
      setActive(null)
    }, [setIsOpen, setActive])

    useClickOutside(ref, handleClickOutside)

    useEffect(() => {
      if (!widthContainer || maxWidth > 0) return
      setMaxWidth(widthContainer)
    }, [widthContainer, maxWidth])

    const scrollButtonIntoView = useCallback(
      (currentIndex: number, previousIndex: number | null) => {
        if (!menuContainerRef.current) return

        const isMovingForward =
          previousIndex !== null && currentIndex > previousIndex
        const isMovingBackward =
          previousIndex !== null && currentIndex < previousIndex

        let targetIndex = currentIndex

        if (isMovingForward) {
          const nextIndex = currentIndex + 1
          if (nextIndex < steps.length) {
            targetIndex = nextIndex
          }
        } else if (isMovingBackward) {
          const prevIndex = currentIndex - 1
          if (prevIndex >= 0) {
            targetIndex = prevIndex
          }
        }

        const targetButton = menuContainerRef.current.querySelector(
          `[data-step-index="${targetIndex}"]`
        ) as HTMLElement

        if (targetButton) {
          targetButton.scrollIntoView({
            behavior: "smooth",
            block: "nearest",
            inline: "center",
          })
        }
      },
      [steps.length]
    )

    const handleNavClick = useCallback(
      (item: string) => {
        if (active === item && isOpen) {
          setIsOpen(false)
          setActive(null)
          return
        }

        const currentIndex = steps.findIndex((step) => step.id === item)
        setActive(item)
        setIsOpen(true)

        if (currentIndex >= 0) {
          setTimeout(() => {
            scrollButtonIntoView(currentIndex, previousIndex)
          }, 100)

          setPreviousIndex(currentIndex)
        }
      },
      [
        active,
        isOpen,
        steps,
        scrollButtonIntoView,
        previousIndex,
        setActive,
        setIsOpen,
      ]
    )

    const renderContent = useCallback(() => {
      if (!active) return null

      const step = steps.find((s) => s.id === active)
      if (!step) return null

      return (
        <div className="space-y-4 pb-3">
          <div className="space-y-2">
            <h3 className="text-foreground text-lg font-medium">
              {step.title}
            </h3>
            <p className="text-muted-foreground text-sm">{step.description}</p>
          </div>
          {step.content}
        </div>
      )
    }, [active, steps])

    const activeTitle = useMemo(() => {
      if (!active) return ""
      const step = steps.find((s) => s.id === active)
      return step?.title || ""
    }, [active, steps])

    const navigationButtons = useMemo(
      () =>
        steps.map((step, index) => ({
          id: step.id,
          label: step.title,
          step: (index + 1).toString(),
          onClick: () => handleNavClick(step.id),
          isActive: active === step.id,
          isFirst: index === 0,
          isLast: index === steps.length - 1,
        })),
      [steps, active, handleNavClick]
    )

    return (
      <div
        className={cn(
          "mx-auto w-full max-w-sm space-y-2 px-2 sm:max-w-lg sm:px-0",
          className
        )}
      >
        {badgeText && (
          <Badge
            variant="outline"
            className="bg-muted border-border text-muted-foreground"
          >
            {badgeText}
          </Badge>
        )}

        <MotionConfig transition={transition}>
          <div
            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)]"
            ref={ref}
          >
            <div className="rounded-2xl">
              <div className="overflow-hidden">
                <AnimatePresence initial={false} mode="wait">
                  {isOpen ? (
                    <motion.div
                      key="expanded-content"
                      initial={{ height: 0 }}
                      animate={{ height: heightContent || 0 }}
                      exit={{ height: 0 }}
                      className=""
                    >
                      <div ref={contentRef} className="px-2 pt-2 sm:px-2">
                        <h4 className="text-foreground px-2 text-sm font-medium">
                          {activeTitle}
                        </h4>

                        <div className="pb-1">
                          <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)]">
                            {renderContent()}
                          </div>
                        </div>
                      </div>
                    </motion.div>
                  ) : null}
                </AnimatePresence>
              </div>

              {/* Navigation bar */}
              <div className="relative z-10">
                <ScrollArea
                  className="w-full"
                  viewportClassName="scrollbar-hide"
                  maskHeight={16}
                  ref={(element) => {
                    if (element) {
                      menuContainerRef.current = element
                      menuRef(element)
                    }
                  }}
                >
                  <div className="flex w-max min-w-full items-center p-[1px]">
                    {navigationButtons.map((button, index) => (
                      <button
                        key={button.id}
                        data-step-index={index}
                        onClick={button.onClick}
                        className={cn(
                          "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",
                          button.isActive
                            ? "text-foreground bg-muted/50 font-medium"
                            : "hover:bg-muted/60 active:bg-muted/70"
                        )}
                      >
                        <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)]">
                          <span
                            className={cn(
                              "w-full rounded-md",
                              button.isActive
                                ? "bg-blue-300/20 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400"
                                : "bg-muted/50 text-muted-foreground"
                            )}
                          >
                            {button.step}
                          </span>
                        </div>
                        <span className="text-foreground text-xs font-medium sm:text-sm">
                          {button.label}
                        </span>
                      </button>
                    ))}
                  </div>
                </ScrollArea>
              </div>
            </div>
          </div>
        </MotionConfig>
      </div>
    )
  }
)

DynamicToolbarExpandable.displayName = "DynamicToolbarExpandable"

// ________________________ HOOKS ________________________
interface Bounds {
  left: number
  top: number
  width: number
  height: number
}

function useMeasure(): [
  (node: HTMLElement | null) => void,
  Bounds,
  () => void,
] {
  const [bounds, setBounds] = useState<Bounds>({
    left: 0,
    top: 0,
    width: 0,
    height: 0,
  })

  const [node, setNode] = useState<HTMLElement | null>(null)
  const observer = useRef<ResizeObserver | null>(null)

  const disconnect = useCallback(() => {
    if (observer.current) {
      observer.current.disconnect()
    }
  }, [])

  const ref = useCallback((node: HTMLElement | null) => {
    setNode(node)
  }, [])

  useEffect(() => {
    if (!node) return

    if (observer.current) {
      observer.current.disconnect()
    }

    observer.current = new ResizeObserver(([entry]) => {
      if (entry && entry.contentRect) {
        const { left, top, width, height } = entry.contentRect
        setBounds({ left, top, width, height })
      }
    })

    observer.current.observe(node)

    return () => {
      if (observer.current) {
        observer.current.disconnect()
      }
    }
  }, [node])

  return [ref, bounds, disconnect]
}

function useClickOutside<T extends HTMLElement = HTMLElement>(
  ref: RefObject<T | null>,
  handler: (event: MouseEvent | TouchEvent) => void
) {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      const el = ref?.current
      if (!el || el.contains((event?.target as Node) || null)) {
        return
      }

      handler(event)
    }

    document.addEventListener("mousedown", listener)
    document.addEventListener("touchstart", listener)

    return () => {
      document.removeEventListener("mousedown", listener)
      document.removeEventListener("touchstart", listener)
    }
  }, [ref, handler])
}

function useTouchPrimary() {
  const [isTouchPrimary, setIsTouchPrimary] = useState(false)

  useEffect(() => {
    if (typeof window === "undefined") return

    const controller = new AbortController()
    const { signal } = controller

    const handleTouch = () => {
      const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0
      const prefersTouch = window.matchMedia("(pointer: coarse)").matches
      setIsTouchPrimary(hasTouch && prefersTouch)
    }

    const mq = window.matchMedia("(pointer: coarse)")
    mq.addEventListener("change", handleTouch, { signal })
    window.addEventListener("pointerdown", handleTouch, { signal })

    handleTouch()

    return () => controller.abort()
  }, [])

  return isTouchPrimary
}

// ________________________ MODIFIED SCROLL AREA ________________________
// https://lina.sameer.sh/

const ScrollAreaContext = React.createContext<boolean>(false)
type Mask = {
  top: boolean
  bottom: boolean
  left: boolean
  right: boolean
}

const ScrollArea = React.forwardRef<
  React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
    viewportClassName?: string
    /**
     * `maskHeight` is the height of the mask in pixels.
     * pass `0` to disable the mask
     * @default 30
     */
    maskHeight?: number
    maskClassName?: string
  }
>(
  (
    {
      className,
      children,
      scrollHideDelay = 0,
      viewportClassName,
      maskClassName,
      maskHeight = 30,
      ...props
    },
    ref
  ) => {
    const [showMask, setShowMask] = React.useState<Mask>({
      top: false,
      bottom: false,
      left: false,
      right: false,
    })
    const viewportRef = React.useRef<HTMLDivElement>(null)
    const isTouch = useTouchPrimary()

    const checkScrollability = React.useCallback(() => {
      const element = viewportRef.current
      if (!element) return

      const {
        scrollTop,
        scrollLeft,
        scrollWidth,
        clientWidth,
        scrollHeight,
        clientHeight,
      } = element
      setShowMask((prev) => ({
        ...prev,
        top: scrollTop > 0,
        bottom: scrollTop + clientHeight < scrollHeight - 1,
        left: scrollLeft > 0,
        right: scrollLeft + clientWidth < scrollWidth - 1,
      }))
    }, [])

    React.useEffect(() => {
      if (typeof window === "undefined") return

      const element = viewportRef.current
      if (!element) return

      const controller = new AbortController()
      const { signal } = controller

      const resizeObserver = new ResizeObserver(checkScrollability)
      resizeObserver.observe(element)

      element.addEventListener("scroll", checkScrollability, { signal })
      window.addEventListener("resize", checkScrollability, { signal })

      checkScrollability()

      return () => {
        controller.abort()
        resizeObserver.disconnect()
      }
    }, [checkScrollability, isTouch])

    return (
      <ScrollAreaContext.Provider value={isTouch}>
        {isTouch ? (
          <div
            ref={ref}
            role="group"
            data-slot="scroll-area"
            aria-roledescription="scroll area"
            className={cn("relative overflow-hidden", className)}
            {...props}
          >
            <div
              ref={viewportRef}
              data-slot="scroll-area-viewport"
              className={cn(
                "size-full overflow-auto rounded-[inherit]",
                viewportClassName
              )}
              tabIndex={0}
            >
              {children}
            </div>

            {maskHeight > 0 && (
              <ScrollMask
                showMask={showMask}
                className={maskClassName}
                maskHeight={maskHeight}
              />
            )}
          </div>
        ) : (
          <ScrollAreaPrimitive.Root
            ref={ref}
            data-slot="scroll-area"
            scrollHideDelay={scrollHideDelay}
            className={cn("relative overflow-hidden", className)}
            {...props}
          >
            <ScrollAreaPrimitive.Viewport
              ref={viewportRef}
              data-slot="scroll-area-viewport"
              className={cn("size-full rounded-[inherit]", viewportClassName)}
            >
              {children}
            </ScrollAreaPrimitive.Viewport>

            {maskHeight > 0 && (
              <ScrollMask
                showMask={showMask}
                className={maskClassName}
                maskHeight={maskHeight}
              />
            )}
            <ScrollBar />
            <ScrollAreaPrimitive.Corner />
          </ScrollAreaPrimitive.Root>
        )}
      </ScrollAreaContext.Provider>
    )
  }
)

ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
  React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => {
  const isTouch = React.useContext(ScrollAreaContext)

  if (isTouch) return null

  return (
    <ScrollAreaPrimitive.ScrollAreaScrollbar
      ref={ref}
      orientation={orientation}
      data-slot="scroll-area-scrollbar"
      className={cn(
        "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",
        orientation === "vertical" &&
          "h-full w-2.5 border-l border-l-transparent",
        orientation === "horizontal" &&
          "h-2.5 flex-col border-t border-t-transparent px-1 pr-1.25",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.ScrollAreaThumb
        data-slot="scroll-area-thumb"
        className={cn(
          "bg-border relative flex-1 origin-center rounded-full transition-[scale]",
          orientation === "vertical" && "my-1 active:scale-y-95",
          orientation === "horizontal" && "active:scale-x-98"
        )}
      />
    </ScrollAreaPrimitive.ScrollAreaScrollbar>
  )
})

ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

const ScrollMask = ({
  showMask,
  maskHeight,
  className,
  ...props
}: React.ComponentProps<"div"> & {
  showMask: Mask
  maskHeight: number
}) => {
  return (
    <>
      <div
        {...props}
        aria-hidden="true"
        style={
          {
            "--top-fade-height": showMask.top ? `${maskHeight}px` : "0px",
            "--bottom-fade-height": showMask.bottom ? `${maskHeight}px` : "0px",
          } as React.CSSProperties
        }
        className={cn(
          "pointer-events-none absolute inset-0 z-10",
          "before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']",
          "after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']",
          "before:h-(--top-fade-height) after:h-(--bottom-fade-height)",
          showMask.top ? "before:opacity-100" : "before:opacity-0",
          showMask.bottom ? "after:opacity-100" : "after:opacity-0",
          "before:from-background before:bg-gradient-to-b before:to-transparent",
          "after:from-background after:bg-gradient-to-t after:to-transparent",
          className
        )}
      />
      <div
        {...props}
        aria-hidden="true"
        style={
          {
            "--left-fade-width": showMask.left ? `${maskHeight}px` : "0px",
            "--right-fade-width": showMask.right ? `${maskHeight}px` : "0px",
          } as React.CSSProperties
        }
        className={cn(
          "pointer-events-none absolute inset-0 z-10",
          "before:absolute before:inset-y-0 before:left-0 before:transition-[width,opacity] before:duration-300 before:content-['']",
          "after:absolute after:inset-y-0 after:right-0 after:transition-[width,opacity] after:duration-300 after:content-['']",
          "before:w-(--left-fade-width) after:w-(--right-fade-width)",
          showMask.left ? "before:opacity-100" : "before:opacity-0",
          showMask.right ? "after:opacity-100" : "after:opacity-0",
          "before:from-background before:bg-gradient-to-r before:to-transparent",
          "after:from-background after:bg-gradient-to-l after:to-transparent",
          className
        )}
      />
    </>
  )
}

export default function ToolbarExpandable(
  props: DynamicToolbarExpandableProps
) {
  return <DynamicToolbarExpandable {...props} />
}
```

**Update the import paths to match your project setup.**

## Usage

```tsx
import ToolbarExpandable from "@/components/ui/toolbar-expandable";
```

### Basic Usage

```tsx
const steps = [
  {
    id: "step1",
    title: "First Step",
    description: "Description of the first step",
    icon: Settings,
    content: <div>Step 1 content</div>,
  },
  // ... more steps
]

<ToolbarExpandable steps={steps} badgeText="WORKFLOW" />
```

### Controlled Usage

```tsx

const [expanded, setExpanded] = useState(false)
const [activeStep, setActiveStep] = useState<string | null>(null)

<ToolbarExpandable
  steps={steps}
  badgeText="CONTROLLED"
  expanded={expanded}
  onExpandedChange={setExpanded}
  activeStep={activeStep}
  onActiveStepChange={setActiveStep}
/>
```

## API Reference

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `steps` | `DynamicStep[]` | - | Array of step objects defining the toolbar content |
| `badgeText` | `string` | - | Text displayed in the badge |
| `className` | `string` | - | Additional CSS classes |
| `expanded` | `boolean` | - | Controlled expanded state |
| `onExpandedChange` | `(expanded: boolean) => void` | - | Callback for expanded state changes |
| `activeStep` | `string \| null` | - | Controlled active step ID |
| `onActiveStepChange` | `(stepId: string \| null) => void` | - | Callback for active step changes |

### DynamicStep Interface

```tsx
interface DynamicStep {
  id: string;
  title: string;
  description: string;
  icon:
    | React.ComponentType<{ className?: string }>
    | React.ReactElement<{ className?: string }>;
  content: React.ReactNode;
}
```

| Property | Type | Description |
| --- | --- | --- |
| `id` | `string` | Unique identifier for the step |
| `title` | `string` | Display title for the step |
| `description` | `string` | Description text for the step |
| `icon` | `React.ComponentType` or `React.ReactElement` | Icon component or element |
| `content` | `React.ReactNode` | Content to display when step is active |

## Examples

### Deployment Workflow

```tsx
const deploymentSteps = [
  {
    id: "setup",
    title: "Project Setup",
    description: "Initialize your project with the required dependencies.",
    icon: Settings,
    content: <ProjectSetupForm />,
  },
  {
    id: "configure",
    title: "Configuration",
    description: "Set up environment variables and project settings.",
    icon: Database,
    content: <ConfigurationForm />,
  },
  {
    id: "deploy",
    title: "Deploy",
    description: "Deploy your application to production.",
    icon: Rocket,
    content: <DeploymentForm />,
  },
]

<ToolbarExpandable steps={deploymentSteps} badgeText="DEPLOYMENT" />
```

### Download Configuration

```tsx
const downloadSteps = [
  {
    id: "format",
    title: "Choose Format",
    description: "Select the download format that best suits your needs.",
    icon: Download,
    content: <FormatSelector />,
  },
  {
    id: "options",
    title: "Download Options",
    description: "Configure additional options for your download.",
    icon: Settings,
    content: <OptionsForm />,
  },
]

<ToolbarExpandable steps={downloadSteps} badgeText="DOWNLOAD" />
```

## Dependencies

The component requires the following dependencies:

- `motion` - For animations
- `@radix-ui/react-scroll-area` - For enhanced scrolling
- `@/components/ui/badge` - For the badge component

## Accessibility

The component includes several accessibility features:

- Proper ARIA roles and descriptions
- Keyboard navigation support
- Screen reader friendly
- Focus management
- Touch-friendly interactions

## Performance

The component is optimized for performance with:

- Memoized components to prevent unnecessary re-renders
- Efficient state management
- Optimized animations
- Lazy loading of content
- Minimal DOM updates

## Browser Support

The component supports all modern browsers and includes:

- Touch device optimization
- Fallbacks for older browsers
- Progressive enhancement
- Responsive design patterns
