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.
Follow these simple steps to add the Progressive Carousel component to your project:
Install Dependencies
pnpm add motion
Create a new file: components/motions/progressive-carousel.tsx
and copy the code below:
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
ReactNode,
FC,
} from "react";
import { motion, AnimatePresence } from "motion/react";
import { cn } from "@/lib/utils";
// Define the type for the context value
interface ProgressSliderContextType {
active: string;
progress: number;
handleButtonClick: (value: string) => void;
vertical: boolean;
}
// Define the type for the component props
interface ProgressSliderProps {
children: ReactNode;
duration?: number;
fastDuration?: number;
vertical?: boolean;
activeSlider: string;
className?: string;
}
interface SliderContentProps {
children: ReactNode;
className?: string;
}
interface SliderWrapperProps {
children: ReactNode;
value: string;
className?: string;
}
interface ProgressBarProps {
children: ReactNode;
className?: string;
}
interface SliderBtnProps {
children: ReactNode;
value: string;
className?: string;
progressBarClass?: string;
}
// Create the context with an undefined initial value
const ProgressSliderContext = createContext<
ProgressSliderContextType | undefined
>(undefined);
export const useProgressSliderContext = (): ProgressSliderContextType => {
const context = useContext(ProgressSliderContext);
if (!context) {
throw new Error(
"useProgressSliderContext must be used within a ProgressSlider",
);
}
return context;
};
export const ProgressSlider: FC<ProgressSliderProps> = ({
children,
duration = 5000,
fastDuration = 400,
vertical = false,
activeSlider,
className,
}) => {
const [active, setActive] = useState<string>(activeSlider);
const [progress, setProgress] = useState<number>(0);
const [isFastForward, setIsFastForward] = useState<boolean>(false);
const frame = useRef<number>(0);
const firstFrameTime = useRef<number>(performance.now());
const targetValue = useRef<string | null>(null);
const [sliderValues, setSliderValues] = useState<string[]>([]);
useEffect(() => {
const getChildren = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === SliderContent,
) as React.ReactElement<SliderContentProps> | undefined;
if (getChildren) {
const values = React.Children.toArray(
getChildren.props.children,
).map(
(child) => (child as React.ReactElement<SliderWrapperProps>).props.value,
);
setSliderValues(values);
}
}, [children]);
useEffect(() => {
if (sliderValues.length > 0) {
firstFrameTime.current = performance.now();
frame.current = requestAnimationFrame(animate);
}
return () => {
cancelAnimationFrame(frame.current);
};
}, [sliderValues, active, isFastForward]);
const animate = (now: number) => {
const currentDuration = isFastForward ? fastDuration : duration;
const elapsedTime = now - firstFrameTime.current;
const timeFraction = elapsedTime / currentDuration;
if (timeFraction <= 1) {
setProgress(
isFastForward
? progress + (100 - progress) * timeFraction
: timeFraction * 100,
);
frame.current = requestAnimationFrame(animate);
} else {
if (isFastForward) {
setIsFastForward(false);
if (targetValue.current !== null) {
setActive(targetValue.current);
targetValue.current = null;
}
} else {
// Move to the next slide
const currentIndex = sliderValues.indexOf(active);
const nextIndex = (currentIndex + 1) % sliderValues.length;
setActive(sliderValues[nextIndex]);
}
setProgress(0);
firstFrameTime.current = performance.now();
}
};
const handleButtonClick = (value: string) => {
if (value !== active) {
const elapsedTime = performance.now() - firstFrameTime.current;
const currentProgress = (elapsedTime / duration) * 100;
setProgress(currentProgress);
targetValue.current = value;
setIsFastForward(true);
firstFrameTime.current = performance.now();
}
};
return (
<ProgressSliderContext.Provider
value={{ active, progress, handleButtonClick, vertical }}
>
<div className={cn("relative", className)}>{children}</div>
</ProgressSliderContext.Provider>
);
};
export const SliderContent: FC<SliderContentProps> = ({
children,
className,
}) => {
return <div className={cn("", className)}>{children}</div>;
};
export const SliderWrapper: FC<SliderWrapperProps> = ({
children,
value,
className,
}) => {
const { active } = useProgressSliderContext();
return (
<AnimatePresence mode="popLayout">
{active === value && (
<motion.div
key={value}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn("", className)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
export const SliderBtnGroup: FC<ProgressBarProps> = ({
children,
className,
}) => {
return <div className={cn("", className)}>{children}</div>;
};
export const SliderBtn: FC<SliderBtnProps> = ({
children,
value,
className,
progressBarClass,
}) => {
const { active, progress, handleButtonClick, vertical } =
useProgressSliderContext();
return (
<button
className={cn(
`relative ${active === value ? "opacity-100" : "opacity-50"}`,
className,
)}
onClick={() => handleButtonClick(value)}
>
{children}
<div
className="absolute inset-0 -z-10 max-h-full max-w-full overflow-hidden"
role="progressbar"
aria-valuenow={active === value ? progress : 0}
>
<span
className={cn("absolute left-0", progressBarClass)}
style={{
[vertical ? "height" : "width"]:
active === value ? `${progress}%` : "0%",
}}
/>
</div>
</button>
);
};
Adjust the import paths in both files according to your project's structure.