Progressive Carousel
An animated progressive carousel built with Framer Motion, offering smooth and dynamic transitions between items.
An animated progressive carousel built with Framer Motion, offering smooth and dynamic transitions between items.
npm install framer-motion
1import React, {2createContext,3useContext,4useState,5useEffect,6useRef,7ReactNode,8FC,9} from 'react';10import { motion, AnimatePresence } from 'framer-motion';11import { cn } from '@/lib/utils';1213// Define the type for the context value14interface ProgressSliderContextType {15active: string;16progress: number;17handleButtonClick: (value: string) => void;18vertical: boolean;19}2021// Define the type for the component props22interface ProgressSliderProps {23children: ReactNode;24duration?: number;25fastDuration?: number;26vertical?: boolean;27activeSlider: string;28className?: string;29}3031interface SliderContentProps {32children: ReactNode;33className?: string;34}3536interface SliderWrapperProps {37children: ReactNode;38value: string;39className?: string;40}4142interface ProgressBarProps {43children: ReactNode;44className?: string;45}4647interface SliderBtnProps {48children: ReactNode;49value: string;50className?: string;51progressBarClass?: string;52}5354// Create the context with an undefined initial value55const ProgressSliderContext = createContext<56ProgressSliderContextType | undefined57>(undefined);5859export const useProgressSliderContext = (): ProgressSliderContextType => {60const context = useContext(ProgressSliderContext);61if (!context) {62throw new Error(63'useProgressSliderContext must be used within a ProgressSlider'64);65}66return context;67};6869export const ProgressSlider: FC<ProgressSliderProps> = ({70children,71duration = 5000,72fastDuration = 400,73vertical = false,74activeSlider,75className,76}) => {77const [active, setActive] = useState<string>(activeSlider);78const [progress, setProgress] = useState<number>(0);79const [isFastForward, setIsFastForward] = useState<boolean>(false);80const frame = useRef<number>(0);81const firstFrameTime = useRef<number>(performance.now());82const targetValue = useRef<string | null>(null);83const [sliderValues, setSliderValues] = useState<string[]>([]);8485useEffect(() => {86const getChildren = React.Children.toArray(children).find(87(child) => (child as React.ReactElement).type === SliderContent88) as React.ReactElement | undefined;8990if (getChildren) {91const values = React.Children.toArray(getChildren.props.children).map(92(child) => (child as React.ReactElement).props.value as string93);94setSliderValues(values);95}96}, [children]);9798useEffect(() => {99if (sliderValues.length > 0) {100firstFrameTime.current = performance.now();101frame.current = requestAnimationFrame(animate);102}103return () => {104cancelAnimationFrame(frame.current);105};106}, [sliderValues, active, isFastForward]);107108const animate = (now: number) => {109const currentDuration = isFastForward ? fastDuration : duration;110const elapsedTime = now - firstFrameTime.current;111const timeFraction = elapsedTime / currentDuration;112113if (timeFraction <= 1) {114setProgress(115isFastForward116? progress + (100 - progress) * timeFraction117: timeFraction * 100118);119frame.current = requestAnimationFrame(animate);120} else {121if (isFastForward) {122setIsFastForward(false);123if (targetValue.current !== null) {124setActive(targetValue.current);125targetValue.current = null;126}127} else {128// Move to the next slide129const currentIndex = sliderValues.indexOf(active);130const nextIndex = (currentIndex + 1) % sliderValues.length;131setActive(sliderValues[nextIndex]);132}133setProgress(0);134firstFrameTime.current = performance.now();135}136};137138const handleButtonClick = (value: string) => {139if (value !== active) {140const elapsedTime = performance.now() - firstFrameTime.current;141const currentProgress = (elapsedTime / duration) * 100;142setProgress(currentProgress);143targetValue.current = value;144setIsFastForward(true);145firstFrameTime.current = performance.now();146}147};148149return (150<ProgressSliderContext.Provider151value={{ active, progress, handleButtonClick, vertical }}152>153<div className={cn('relative', className)}>{children}</div>154</ProgressSliderContext.Provider>155);156};157158export const SliderContent: FC<SliderContentProps> = ({159children,160className,161}) => {162return <div className={cn('', className)}>{children}</div>;163};164165export const SliderWrapper: FC<SliderWrapperProps> = ({166children,167value,168className,169}) => {170const { active } = useProgressSliderContext();171172return (173<AnimatePresence mode='popLayout'>174{active === value && (175<motion.div176key={value}177initial={{ opacity: 0 }}178animate={{ opacity: 1 }}179exit={{ opacity: 0 }}180className={cn('', className)}181>182{children}183</motion.div>184)}185</AnimatePresence>186);187};188189export const SliderBtnGroup: FC<ProgressBarProps> = ({190children,191className,192}) => {193return <div className={cn('', className)}>{children}</div>;194};195196export const SliderBtn: FC<SliderBtnProps> = ({197children,198value,199className,200progressBarClass,201}) => {202const { active, progress, handleButtonClick, vertical } =203useProgressSliderContext();204205return (206<button207className={cn(208`relative ${active === value ? 'opacity-100' : 'opacity-50'}`,209className210)}211onClick={() => handleButtonClick(value)}212>213{children}214<div215className='absolute inset-0 overflow-hidden -z-10 max-h-full max-w-full '216role='progressbar'217aria-valuenow={active === value ? progress : 0}218>219<span220className={cn('absolute left-0 ', progressBarClass)}221style={{222[vertical ? 'height' : 'width']:223active === value ? `${progress}%` : '0%',224}}225/>226</div>227</button>228);229};
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | The content to be displayed within the slider. | |
duration | number | 5000 | The duration of the slider's transition in milliseconds. |
fastDuration | number | 400 | The duration of the slider's fast transition in milliseconds. |
vertical | boolean | false | Whether the slider is oriented vertically. |
activeSlider | string | The identifier of the currently active slider. | |
className | string | Optional CSS class for styling the slider component. |