Vertical Progressive Carousel
Vertical Progressive Carousel é um componente de feedback para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
UI Layouts
MIT
feedback
O que é
Vertical Progressive Carousel é um componente de feedback para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Vertical Progressive Carousel
import { cn } from '@/lib/utils';
import { AnimatePresence, motion } from 'motion/react';
import React, {
createContext,
type FC,
type ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react';
// 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<any>).type === SliderContent
) as React.ReactElement<any> | undefined;
if (getChildren) {
const values = React.Children.toArray(getChildren.props.children).map(
(child) => (child as React.ReactElement<any>).props.value as string
);
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 overflow-hidden -z-10 max-h-full max-w-full '
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>
);
};
'use client';
import {
ProgressSlider,
SliderBtn,
SliderBtnGroup,
SliderContent,
SliderWrapper,
} from '@/components/ui/progressive-carousel';
import { imgPreview } from '@/components/website/constant';
import { useMediaQuery } from '@/hooks/use-media-query';
import { Home } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import Image, { StaticImageData } from 'next/image';
import { useEffect, useRef, useState } from 'react';
const items = [
{
img: 'https://images.unsplash.com/photo-1709949908058-a08659bfa922?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
title: 'Bridge',
desc: 'A breathtaking view of a city illuminated by countless lights, showcasing the vibrant and bustling nightlife.',
sliderName: 'bridge',
},
{
img: 'https://images.unsplash.com/photo-1518972734183-c5b490a7c637?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
title: 'Mountains View',
desc: 'A serene lake reflecting the surrounding mountains and trees, creating a mirror-like surface.',
sliderName: 'mountains',
},
{
img: 'https://images.unsplash.com/photo-1548192746-dd526f154ed9?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
title: 'Autumn',
desc: 'A picturesque path winding through a dense forest adorned with vibrant autumn foliage.',
sliderName: 'autumn',
},
{
img: 'https://images.unsplash.com/photo-1628965882741-570e75becd5d?q=80&w=687&auto=format&fit=crop',
title: 'Foggy',
sliderName: 'foggy',
desc: 'A stunning foggy view over the foresh, with the sun casting a golden glow across the forest. ',
},
];
export default function VerticleProgressive() {
const isMobile = useMediaQuery('(min-width: 640px)');
return (
<>
<ProgressSlider
vertical={isMobile ? true : false}
fastDuration={300}
duration={4000}
activeSlider='bbridge'
className='sm:flex '
>
<SliderBtnGroup className='sm:relative absolute bottom-0 lg:w-md sm:w-96 w-full z-10 sm:flex sm:flex-col grid grid-cols-2 sm:h-[500px] h-fit sm:dark:bg-black sm:bg-white dark:bg-black/80 bg-white/80 backdrop-blur-md overflow-hidden '>
{items.map((item) => (
<SliderBtn
key={item.sliderName}
value={item?.sliderName}
className='text-left p-3 sm:border-b border sm:pl-5 sm:pb-0 pb-6 sm:flex-1'
progressBarClass='left-0 sm:top-0 bottom-0 dark:bg-white bg-black sm:w-3 sm:h-full h-4 before:h-full before:w-4 before:'
>
<h2 className='relative px-4 rounded-sm w-fit dark:bg-blue-500 bg-black text-white mb-2'>
{item.title}
</h2>
<p className='text-sm font-medium dark:text-slate-200 text-slate-900 line-clamp-2'>
{item.desc}
</p>
</SliderBtn>
))}
</SliderBtnGroup>
<SliderContent className='w-full'>
{items.map((item) => (
<SliderWrapper className='h-full' key={item.sliderName} value={item?.sliderName}>
<Image
className=' h-[500px] object-cover'
src={item.img}
width={1900}
height={1080}
alt={item.desc}
/>
</SliderWrapper>
))}
</SliderContent>
</ProgressSlider>
</>
);
}