Octocat

Sequence

A component for creating timed sequences of content with automatic progression.

Triassic
The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import { cn } from '@/lib/utils/classnames';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '../sequence';
import { eras as CONTENT } from './content';

const SequencePreview = () => {
  return (
    <Sequence className="relative w-160" loop duration={3000}>
      <SequenceItems className="flex gap-2 overflow-y-auto py-2">
        {CONTENT.map((item, index) => (
          <SequenceItem
            key={index}
            className={cn(
              'relative shrink-0 cursor-pointer overflow-hidden rounded-lg border border-background-secondary px-4 py-1 text-sm',
              'flex items-center gap-1.5 whitespace-nowrap',
              'transition-colors hover:bg-background-secondary/30',
              'before:absolute before:inset-0 before:-z-10 before:bg-background-secondary before:content-[""]',
              'before:origin-left before:scale-x-(--progress)'
            )}
          >
            <item.icon size={16} className="-ml-1 shrink-0" />
            {item.title}
          </SequenceItem>
        ))}
      </SequenceItems>
      <SequencePanels className="relative h-64 w-full">
        {CONTENT.map((item, index) => (
          <SequencePanel key={index}>
            <div className="absolute top-0 left-0 flex h-full min-h-64 flex-col justify-between rounded-lg border border-border p-4">
              <div className="font-mono text-foreground-secondary text-sm uppercase">
                {item.title}
              </div>
              <div className="text-pretty pr-8 font-medium text-xl">
                {item.description}
              </div>
            </div>
          </SequencePanel>
        ))}
      </SequencePanels>
    </Sequence>
  );
};

export default SequencePreview;

Dependencies

Source Code

'use client';

import {
  type ComponentPropsWithRef,
  createContext,
  type KeyboardEvent,
  use,
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from 'react';

import {
  InstanceCounterProvider,
  useInstanceCounter,
} from '@/components/instance-counter';
import { Slot } from '@/components/slot';
import { useIntersectionObserver } from '@/foundations/hooks/use-intersection-observer';
import { useTicker } from '@/foundations/hooks/use-ticker';
import { clamp } from '@/lib/math/clamp';

// @types
type ItemState = 'upcoming' | 'current' | 'past';

// @context
interface SequenceContext {
  id: string;
  orientation: 'horizontal' | 'vertical';
  set: (index: number) => void;
  next: () => void;
  previous: () => void;
  getItemState: (index: number) => ItemState;
  getItemId: (index: number, role: 'tab' | 'tabpanel') => string;
  setIsIntersecting: (isIntersecting: boolean) => void;
}

const SequenceContext = createContext<SequenceContext | null>(null);

const useSequenceContext = () => {
  const context = use(SequenceContext);
  if (!context)
    throw new Error('Sequence components must be used within a Sequence');

  return context;
};

// @root
interface SequenceProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange'> {
  currentIndex?: number;
  asChild?: boolean;
  loop?: boolean;
  duration?: number | number[];
  orientation?: 'horizontal' | 'vertical';
  paused?: boolean;
  onChange?: (index: number) => void;
}

const Sequence = ({
  children,
  asChild,
  onChange,
  loop,
  orientation = 'horizontal',
  paused = false,
  currentIndex: currentIndexProp,
  duration,
  ...rest
}: SequenceProps) => {
  const id = useId();
  const ref = useRef<HTMLDivElement>(null);

  const [currentIndex, setCurrentIndex] = useState(0);
  const [numItems, setNumItems] = useState(0);

  const durations = useMemo(() => {
    return Array.isArray(duration)
      ? duration
      : new Array(numItems).fill(duration);
  }, [duration, numItems]);

  const progress = useRef(0);
  const [isIntersecting, setIsIntersecting] = useState(false);

  const getItemId = useCallback(
    (index: number, role: 'tab' | 'tabpanel') => [id, role, index].join('-'),
    [id]
  );

  const getItemState = useCallback(
    (index: number): ItemState => {
      if (index === currentIndex) return 'current';
      if (index > currentIndex || index < 0) return 'upcoming';
      return 'past';
    },
    [currentIndex]
  );

  const handleSetCurrentIndex = useCallback(
    (index: number, preventOnChange: boolean = false) => {
      setCurrentIndex(index);

      if (!preventOnChange) {
        onChange?.(index);
      }

      progress.current = 0;
      if (paused) {
        ref.current?.style.setProperty('--progress', '0');
      }

      if (isIntersecting && ref.current?.contains(document.activeElement)) {
        const id = getItemId(index, 'tab');
        document.getElementById(id)?.focus();
      }
    },
    [onChange, getItemId, isIntersecting, paused]
  );

  const next = useCallback(() => {
    handleSetCurrentIndex((currentIndex + 1) % numItems);
  }, [numItems, currentIndex, handleSetCurrentIndex]);

  const previous = useCallback(() => {
    handleSetCurrentIndex(currentIndex > 0 ? currentIndex - 1 : numItems - 1);
  }, [numItems, currentIndex, handleSetCurrentIndex]);

  const ticker = useTicker((_, delta) => {
    if (!numItems) {
      ref.current?.style.setProperty('--progress', '0');
      return true;
    }

    const duration = durations[currentIndex] || 0;
    progress.current = clamp(0, progress.current + delta / duration, 1);
    ref.current?.style.setProperty('--progress', progress.current.toString());

    if (progress.current === 1 && (loop || currentIndex < numItems - 1)) {
      next();
    }

    return progress.current < 1;
  });

  // handle pause/play
  useEffect(() => {
    if (isIntersecting && ticker.paused && !paused) {
      ticker.start();
    }

    if ((!isIntersecting || paused) && !ticker.paused) {
      ticker.stop();
    }
  }, [isIntersecting, ticker, paused]);

  // handle external control
  useEffect(() => {
    if (currentIndexProp !== undefined && currentIndexProp !== currentIndex) {
      handleSetCurrentIndex(currentIndexProp, true);
    }
  }, [handleSetCurrentIndex, currentIndexProp, currentIndex]);

  const Comp = asChild ? Slot : 'div';
  return (
    <SequenceContext.Provider
      value={{
        id,
        orientation,
        set: handleSetCurrentIndex,
        getItemState,
        getItemId,
        next,
        previous,
        setIsIntersecting,
      }}
    >
      <InstanceCounterProvider onChange={setNumItems}>
        <Comp
          {...rest}
          ref={ref}
          style={{ '--progress': 0, '--index': currentIndex, ...rest.style }}
        >
          {children}
        </Comp>
      </InstanceCounterProvider>
    </SequenceContext.Provider>
  );
};

// @items
interface SequenceItemsProps extends ComponentPropsWithRef<'div'> {
  asChild?: boolean;
}

const SequenceItems = ({ children, asChild, ...rest }: SequenceItemsProps) => {
  const { orientation, setIsIntersecting } = useSequenceContext();
  const [ref] = useIntersectionObserver<HTMLDivElement>({}, setIsIntersecting);

  const Comp = asChild ? Slot : 'div';
  return (
    <Comp
      {...rest}
      ref={ref}
      role="tablist"
      aria-orientation={orientation}
      aria-live="polite"
    >
      {children}
    </Comp>
  );
};

// @item
interface SequenceItemProps extends ComponentPropsWithRef<'button'> {
  asChild?: boolean;
}

const SequenceItem = ({
  children,
  asChild,
  onClick,
  onKeyDown,
  ...rest
}: SequenceItemProps) => {
  const { orientation, set, next, previous, getItemState, getItemId } =
    useSequenceContext();

  const index = useInstanceCounter();
  const state = getItemState(index);

  const handleKeyboardNavigation = (e: KeyboardEvent<HTMLButtonElement>) => {
    if (e.key === 'Enter' || e.key === ' ') {
      set(index);
    }

    const keyOrientationMap = {
      horizontal: { next: 'ArrowRight', prev: 'ArrowLeft' },
      vertical: { next: 'ArrowDown', prev: 'ArrowUp' },
    };

    if (keyOrientationMap[orientation].next === e.key) {
      e.preventDefault();
      next();
    }

    if (keyOrientationMap[orientation].prev === e.key) {
      e.preventDefault();
      previous();
    }
  };

  const Comp = asChild ? Slot : 'button';
  return (
    <Comp
      {...rest}
      id={getItemId(index, 'tab')}
      role="tab"
      aria-selected={state === 'current'}
      aria-controls={getItemId(index, 'tabpanel')}
      data-state={state}
      data-selected={state === 'current'}
      tabIndex={state === 'current' ? 0 : -1}
      style={{
        ...(state !== 'current' && { '--progress': +(state === 'past') }),
        ...rest.style,
      }}
      onClick={(e) => {
        onClick?.(e);
        if (!e.defaultPrevented) set(index);
      }}
      onKeyDown={(e) => {
        onKeyDown?.(e);
        if (!e.defaultPrevented) handleKeyboardNavigation(e);
      }}
    >
      {children}
    </Comp>
  );
};

// @panels
interface SequencePanelsProps extends ComponentPropsWithRef<'div'> {
  asChild?: boolean;
}

const SequencePanels = ({
  children,
  asChild,
  ...rest
}: SequencePanelsProps) => {
  const Comp = asChild ? Slot : 'div';
  return (
    <Comp {...rest}>
      <InstanceCounterProvider>{children}</InstanceCounterProvider>
    </Comp>
  );
};

// @panel
interface SequencePanelProps extends ComponentPropsWithRef<'div'> {
  asChild?: boolean;
  forceMount?: boolean;
}

const SequencePanel = ({
  children,
  asChild,
  forceMount,
  ...rest
}: SequencePanelProps) => {
  const { getItemState, getItemId } = useSequenceContext();

  const index = useInstanceCounter();
  const state = getItemState(index);

  const Comp = asChild ? Slot : 'div';
  return (
    <Comp
      {...rest}
      id={getItemId(index, 'tabpanel')}
      data-state={state}
      data-selected={state === 'current'}
      role="tabpanel"
      aria-labelledby={getItemId(index, 'tab')}
      aria-hidden={state !== 'current'}
      inert={state !== 'current'}
      style={{
        ...(state !== 'current' && { '--progress': +(state === 'past') }),
        ...rest.style,
      }}
    >
      {forceMount || state === 'current' ? children : null}
    </Comp>
  );
};

export { Sequence, SequenceItem, SequenceItems, SequencePanel, SequencePanels };

Features

  • Automatic Progression: Items advance automatically based on configurable durations
  • Progress Indication: Exposes a --progress css variable to show progression through each item, and a --index css variable to show the current index
  • Keyboard Navigation: Full keyboard support for manual navigation
  • ARIA Support: Full accessibility implementation with proper ARIA attributes
  • Unstyled: No default styles, just the sequence behavior
  • Intersection Observer: Pauses progression when not in viewport

Anatomy


          <Sequence>
  <SequenceItems>
    <SequenceItem />
  </SequenceItems>
  <SequencePanels>
    <SequencePanel />
  </SequencePanels>
</Sequence>
        

API Reference

Sequence

The root container component that manages the state and behavior of the sequence.

Prop Default Type Description
duration * - number | number[] The duration of the sequence in milliseconds. If an array is provided, it will be used to set the duration of each item individually.
orientation "horizontal" "horizontal" | "vertical" The orientation of the sequence.
loop false boolean Whether the sequence should loop.
paused false boolean Whether the sequence should be paused.
currentIndex - number The controlled index of the active item.
onChange - (index: string) => void Callback triggered when the active item changes.
asChild - boolean Whether to merge props onto the child element.

SequenceItems

The container for sequence items.

Prop Default Type Description
asChild - boolean Whether to merge props onto the child element.

SequenceItem

The individual sequence item button component.

Prop Default Type Description
asChild - boolean Whether to merge props onto the child element.

SequencePanels

The container for sequence panels.

Prop Default Type Description
asChild - boolean Whether to merge props onto the child element.

SequencePanel

The individual sequence panel component.

Prop Default Type Description
forceMount false boolean Whether to always mount the panel, regardless of whether it is active.
asChild - boolean Whether to merge props onto the child element.

Examples

Vertical Orientation

The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '@/components/sequence';
import { cn } from '@/lib/utils/classnames';

import { eras as CONTENT } from './content';

const SequenceVertical = () => {
  return (
    <Sequence
      orientation="vertical"
      loop
      className="flex max-w-lg gap-10"
      duration={3000}
    >
      <div className="flex gap-2">
        <div
          className={cn(
            'flex h-8 items-center transition-transform duration-300 ease-out',
            'translate-y-[calc(var(--index)*100%)]'
          )}
        >
          <div
            className={cn(
              'relative h-3 w-3 rounded-full text-foreground',
              'before:absolute before:inset-0 before:bg-foreground/10',
              "before:rounded-full before:content-['']"
            )}
            style={{
              '--fill': 'calc(var(--progress) * 100%)',
              background: `conic-gradient(from 0deg at 50% 50%, currentColor var(--fill), transparent var(--fill))`,
            }}
          />
        </div>
        <SequenceItems>
          {CONTENT.map((item, index) => (
            <SequenceItem
              key={index}
              value={index.toString()}
              className={cn(
                'block h-8 text-left font-medium text-base',
                'text-foreground-secondary data-[selected=true]:text-foreground',
                'cursor-pointer hover:text-foreground/60 active:text-foreground/80'
              )}
            >
              {item.title}
            </SequenceItem>
          ))}
        </SequenceItems>
      </div>
      <SequencePanels className="min-h-64">
        {CONTENT.map((item, index) => (
          <SequencePanel key={index} className="font-medium text-xl">
            {item.description}
          </SequencePanel>
        ))}
      </SequencePanels>
    </Sequence>
  );
};

export default SequenceVertical;

As Child

Triassic
The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import { useState } from 'react';

import { Button } from '@/components/button';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '../sequence';
import { eras as CONTENT } from './content';

const SequencePreview = () => {
  const [selectedIndex, setSelectedIndex] = useState<number>(0);

  return (
    <Sequence
      className="relative w-160"
      loop
      onChange={(index) => setSelectedIndex(index)}
      duration={3000}
    >
      <SequenceItems className="flex gap-2 overflow-y-auto py-2">
        {CONTENT.map((item, index) => (
          <SequenceItem key={index} asChild>
            <Button
              size="sm"
              variant={selectedIndex === index ? 'primary' : 'outline'}
            >
              <item.icon size={16} className="-ml-1 shrink-0" />
              {item.title}
            </Button>
          </SequenceItem>
        ))}
      </SequenceItems>
      <SequencePanels className="relative h-64 w-full">
        {CONTENT.map((item, index) => (
          <SequencePanel key={index}>
            <div className="absolute top-0 left-0 flex h-full min-h-64 flex-col justify-between rounded-lg border border-border p-4">
              <div className="font-mono text-foreground-secondary text-sm uppercase">
                {item.title}
              </div>
              <div className="text-pretty pr-8 font-medium text-xl">
                {item.description}
              </div>
            </div>
          </SequencePanel>
        ))}
      </SequencePanels>
    </Sequence>
  );
};

export default SequencePreview;

Animate Presence

To make AnimatePresence work with SequencePanel and maintain accessibility, you need to jump through a few hoops. Because SequencePanel’s conditional rendering is only applied to its children, you need to force the panel to mount and unmount with a different key to trigger AnimatePresence’s exit animation. This example demonstrates how you would do it.

Triassic
The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import { AnimatePresence, motion } from 'motion/react';
import { useState } from 'react';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '@/components/sequence';
import { cn } from '@/lib/utils/classnames';

import { eras as CONTENT } from './content';

const SequenceMotion = () => {
  const [selectedIndex, setSelectedIndex] = useState<number>(0);

  return (
    <Sequence
      loop
      className="w-160"
      onChange={(index) => setSelectedIndex(index)}
      duration={3000}
    >
      <SequenceItems className="flex w-full gap-2">
        {CONTENT.map((item, index) => (
          <SequenceItem
            key={index}
            className={cn(
              'relative shrink-0 cursor-pointer overflow-hidden rounded-lg border border-background-secondary px-4 py-1 text-sm',
              'flex items-center gap-1.5 whitespace-nowrap',
              'transition-colors hover:bg-background-secondary/30',
              'before:absolute before:inset-0 before:-z-10 before:bg-background-secondary before:content-[""]',
              'before:origin-left before:scale-x-(--progress)'
            )}
          >
            <item.icon size={16} className="-ml-1 shrink-0" />
            {item.title}
          </SequenceItem>
        ))}
      </SequenceItems>

      <SequencePanels className="mt-4 grid overflow-hidden">
        <AnimatePresence mode="wait" initial={false}>
          {CONTENT.map((item, index) =>
            index === selectedIndex ? (
              <motion.div
                key={index}
                className="col-start-1 row-start-1"
                initial={{ scale: 0.95 }}
                animate={{ scale: 1 }}
                exit={{ scale: 0.95 }}
                transition={{ duration: 0.125 }}
              >
                <SequencePanel forceMount>
                  <div className="flex h-full min-h-64 flex-col justify-between rounded-lg border border-border p-4">
                    <motion.div
                      className="font-mono text-foreground-secondary text-sm uppercase"
                      initial={{ opacity: 0, y: '50%' }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{
                        duration: 1.5,
                        delay: 0.15,
                        ease: [0.19, 1.0, 0.22, 1.0],
                      }}
                    >
                      {item.title}
                    </motion.div>
                    <motion.div
                      className="text-pretty pr-8 font-medium text-xl"
                      initial={{ opacity: 0, y: '50%' }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{
                        duration: 1.5,
                        delay: 0.15,
                        ease: [0.19, 1.0, 0.22, 1.0],
                      }}
                    >
                      {item.description}
                    </motion.div>
                  </div>
                </SequencePanel>
              </motion.div>
            ) : (
              <SequencePanel key={`${index}placeholder`} />
            )
          )}
        </AnimatePresence>
      </SequencePanels>
    </Sequence>
  );
};

export default SequenceMotion;

Controlled

Triassic
The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import { useState } from 'react';

import { Button } from '@/components/button';
import { cn } from '@/lib/utils/classnames';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '../sequence';
import { eras as CONTENT } from './content';

const SequenceControlled = () => {
  const [currentIndex, setCurrentIndex] = useState(0);

  return (
    <div>
      <div className="py-2">
        <Button
          size="sm"
          onClick={() => setCurrentIndex((prev) => (prev + 1) % CONTENT.length)}
        >
          Next
        </Button>
      </div>
      <Sequence
        className="relative w-160"
        loop
        duration={3000}
        currentIndex={currentIndex}
        onChange={setCurrentIndex}
      >
        <SequenceItems className="flex gap-2 overflow-y-auto py-2">
          {CONTENT.map((item, index) => (
            <SequenceItem
              key={index}
              className={cn(
                'relative shrink-0 cursor-pointer overflow-hidden rounded-lg border border-background-secondary px-4 py-1 text-sm',
                'flex items-center gap-1.5 whitespace-nowrap',
                'transition-colors hover:bg-background-secondary/30',
                'before:absolute before:inset-0 before:-z-10 before:bg-background-secondary before:content-[""]',
                'before:origin-left before:scale-x-(--progress)'
              )}
            >
              <item.icon size={16} className="-ml-1 shrink-0" />
              {item.title}
            </SequenceItem>
          ))}
        </SequenceItems>
        <SequencePanels className="relative h-64 w-full">
          {CONTENT.map((item, index) => (
            <SequencePanel key={index}>
              <div className="absolute top-0 left-0 flex h-full min-h-64 flex-col justify-between rounded-lg border border-border p-4">
                <div className="font-mono text-foreground-secondary text-sm uppercase">
                  {item.title}
                </div>
                <div className="text-pretty pr-8 font-medium text-xl">
                  {item.description}
                </div>
              </div>
            </SequencePanel>
          ))}
        </SequencePanels>
      </Sequence>
    </div>
  );
};

export default SequenceControlled;

Pause on Hover

Triassic
The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import { useState } from 'react';

import { cn } from '@/lib/utils/classnames';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '../sequence';
import { eras as CONTENT } from './content';

const SequencePauseHover = () => {
  const [paused, setPaused] = useState(false);

  return (
    <Sequence className="relative w-160" loop paused={paused} duration={3000}>
      <SequenceItems
        className="flex gap-2 overflow-y-auto py-2"
        onMouseEnter={() => setPaused(true)}
        onMouseLeave={() => setPaused(false)}
      >
        {CONTENT.map((item, index) => (
          <SequenceItem
            key={index}
            className={cn(
              'relative shrink-0 cursor-pointer overflow-hidden rounded-lg border border-background-secondary px-4 py-1 text-sm',
              'flex items-center gap-1.5 whitespace-nowrap',
              'transition-colors hover:bg-background-secondary/30',
              'before:absolute before:inset-0 before:-z-10 before:bg-background-secondary before:content-[""]',
              'before:origin-left before:scale-x-(--progress)'
            )}
          >
            <item.icon size={16} className="-ml-1 shrink-0" />
            {item.title}
          </SequenceItem>
        ))}
      </SequenceItems>
      <SequencePanels className="relative h-64 w-full">
        {CONTENT.map((item, index) => (
          <SequencePanel key={index}>
            <div className="absolute top-0 left-0 flex h-full min-h-64 flex-col justify-between rounded-lg border border-border p-4">
              <div className="font-mono text-foreground-secondary text-sm uppercase">
                {item.title}
              </div>
              <div className="text-pretty pr-8 font-medium text-xl">
                {item.description}
              </div>
            </div>
          </SequencePanel>
        ))}
      </SequencePanels>
    </Sequence>
  );
};

export default SequencePauseHover;

Scroll Into View

The root Sequence component exposes an elements argument in its onChange prop that allows you to manipulate the active items. This example demonstrates how to scroll the active item into view.

Triassic
The Triassic period (252-201 million years ago) marked the beginning of the dinosaur age, characterized by hot, dry climates and the emergence of the first true dinosaurs.
'use client';

import { useRef } from 'react';

import { scrollIntoViewIfNeeded } from '@/lib/dom/scroll-into-view-if-needed';
import { cn } from '@/lib/utils/classnames';

import {
  Sequence,
  SequenceItem,
  SequenceItems,
  SequencePanel,
  SequencePanels,
} from '../sequence';
import { erasExtended as CONTENT } from './content';

const SequenceScrollIntoView = () => {
  const itemsScrollContainerRef = useRef<HTMLDivElement>(null);

  return (
    <Sequence
      className="relative max-w-[min(90vw,640px)]"
      loop
      duration={3000}
      onChange={(index) => {
        if (itemsScrollContainerRef.current) {
          const item = itemsScrollContainerRef.current.children[index];

          if (item) {
            scrollIntoViewIfNeeded(
              itemsScrollContainerRef.current,
              item as HTMLElement,
              {
                behavior: 'smooth',
              }
            );
          }
        }
      }}
    >
      <SequenceItems asChild>
        <div
          ref={itemsScrollContainerRef}
          className="flex gap-2 overflow-y-auto py-2"
        >
          {CONTENT.map((item, index) => (
            <SequenceItem
              key={index}
              className={cn(
                'relative shrink-0 cursor-pointer overflow-hidden rounded-lg border border-background-secondary px-4 py-1 text-sm',
                'flex items-center gap-1.5 whitespace-nowrap',
                'transition-colors hover:bg-background-secondary/30',
                'before:absolute before:inset-0 before:-z-10 before:bg-background-secondary before:content-[""]',
                'before:origin-left before:scale-x-(--progress)'
              )}
            >
              <item.icon size={16} className="-ml-1 shrink-0" />
              {item.title}
            </SequenceItem>
          ))}
        </div>
      </SequenceItems>
      <SequencePanels className="relative h-64 w-full">
        {CONTENT.map((item, index) => (
          <SequencePanel key={index}>
            <div className="absolute top-0 left-0 flex h-full min-h-64 flex-col justify-between rounded-lg border border-border p-4">
              <div className="font-mono text-foreground-secondary text-sm uppercase">
                {item.title}
              </div>
              <div className="text-pretty pr-8 font-medium text-xl">
                {item.description}
              </div>
            </div>
          </SequencePanel>
        ))}
      </SequencePanels>
    </Sequence>
  );
};

export default SequenceScrollIntoView;

Previous

Marquee

Next

Slot