File size: 3,487 Bytes
bf8b26e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import React, { useState, useRef, useEffect } from 'react';
import { motion } from 'framer-motion';
import { classNames } from '~/utils/classNames';

interface Tab {
  /** Unique identifier for the tab */
  id: string;

  /** Content to display in the tab */
  label: React.ReactNode;

  /** Optional icon to display before the label */
  icon?: string;
}

interface TabsWithSliderProps {
  /** Array of tab objects */
  tabs: Tab[];

  /** ID of the currently active tab */
  activeTab: string;

  /** Function called when a tab is clicked */
  onChange: (tabId: string) => void;

  /** Additional class name for the container */
  className?: string;

  /** Additional class name for inactive tabs */
  tabClassName?: string;

  /** Additional class name for the active tab */
  activeTabClassName?: string;

  /** Additional class name for the slider */
  sliderClassName?: string;
}

/**
 * TabsWithSlider component
 *
 * A tabs component with an animated slider that moves to the active tab.
 */
export function TabsWithSlider({
  tabs,
  activeTab,
  onChange,
  className,
  tabClassName,
  activeTabClassName,
  sliderClassName,
}: TabsWithSliderProps) {
  // State for slider dimensions
  const [sliderDimensions, setSliderDimensions] = useState({ width: 0, left: 0 });

  // Refs for tab elements
  const tabsRef = useRef<(HTMLButtonElement | null)[]>([]);

  // Update slider position when active tab changes
  useEffect(() => {
    const activeIndex = tabs.findIndex((tab) => tab.id === activeTab);

    if (activeIndex !== -1 && tabsRef.current[activeIndex]) {
      const activeTabElement = tabsRef.current[activeIndex];

      if (activeTabElement) {
        setSliderDimensions({
          width: activeTabElement.offsetWidth,
          left: activeTabElement.offsetLeft,
        });
      }
    }
  }, [activeTab, tabs]);

  return (
    <div className={classNames('relative flex gap-2', className)}>
      {/* Tab buttons */}
      {tabs.map((tab, index) => (
        <button
          key={tab.id}
          ref={(el) => (tabsRef.current[index] = el)}
          onClick={() => onChange(tab.id)}
          className={classNames(
            'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center relative overflow-hidden',
            tab.id === activeTab
              ? classNames('text-white shadow-sm shadow-purple-500/20', activeTabClassName)
              : classNames(
                  'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark',
                  tabClassName,
                ),
          )}
        >
          <span className={classNames('flex items-center gap-2', tab.id === activeTab ? 'font-medium' : '')}>
            {tab.icon && <span className={tab.icon} />}
            {tab.label}
          </span>
        </button>
      ))}

      {/* Animated slider */}
      <motion.div
        className={classNames('absolute bottom-0 left-0 h-10 rounded-lg bg-purple-500 -z-10', sliderClassName)}
        initial={false}
        animate={{
          width: sliderDimensions.width,
          x: sliderDimensions.left,
        }}
        transition={{ type: 'spring', stiffness: 300, damping: 30 }}
      />
    </div>
  );
}