UNPKG

lightswind

Version:

A professionally designed animate react component library & templates market that brings together functionality, accessibility, and beautiful aesthetics for modern applications.

241 lines (240 loc) 14.3 kB
"use client"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useCallback, useEffect } from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../lib/utils'; // Assuming this utility correctly merges class names export const TeamCarousel = ({ members, title = "OUR TEAM", titleSize = "2xl", titleColor = "rgba(0, 76, 255, 1)", background, cardWidth = 280, cardHeight = 380, cardRadius = 20, showArrows = true, showDots = true, keyboardNavigation = true, touchNavigation = true, animationDuration = 800, autoPlay = 0, pauseOnHover = true, visibleCards = 2, sideCardScale = 0.9, sideCardOpacity = 0.8, grayscaleEffect = true, className, cardClassName, titleClassName, infoPosition = "bottom", infoTextColor = "rgb(8, 42, 123)", infoBackground = "transparent", onMemberChange, onCardClick, initialIndex = 0, }) => { const [currentIndex, setCurrentIndex] = useState(initialIndex); const [direction, setDirection] = useState(0); // 0: no movement, 1: next, -1: prev const [touchStart, setTouchStart] = useState(0); const [touchEnd, setTouchEnd] = useState(0); const totalMembers = members.length; const paginate = useCallback((newDirection) => { if (totalMembers === 0) return; setDirection(newDirection); const nextIndex = (currentIndex + newDirection + totalMembers) % totalMembers; setCurrentIndex(nextIndex); onMemberChange?.(members[nextIndex], nextIndex); }, [currentIndex, totalMembers, members, onMemberChange]); const wrapIndex = (index) => { return (index + totalMembers) % totalMembers; }; const calculatePosition = (index) => { const activeIndex = currentIndex; const diff = wrapIndex(index - activeIndex); if (diff === 0) return 'center'; if (diff <= visibleCards) return `right-${diff}`; if (diff >= totalMembers - visibleCards) return `left-${totalMembers - diff}`; return 'hidden'; }; // Explicitly type the return of getVariantStyles to match framer-motion's expectations const getVariantStyles = (position) => { // FIX: Changed ease from number[] to an array of string presets or a valid CubicBezier type // Using string presets for simplicity and type compatibility. // If you need the exact cubic-bezier values, ensure they are compatible with framer-motion's Easing type. // For custom cubic-bezier, you might need to use a type assertion like `as [number, number, number, number]` // or import CubicBezier from 'framer-motion/types/value/types'. const transition = { duration: animationDuration / 1000, // You can use a string preset like 'easeInOut' or a valid cubic-bezier array if framer-motion's types support it directly // For the given numbers, 'easeInOut' is a close approximation or 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' if framer-motion accepted it directly as string // To strictly match [0.25, 0.46, 0.45, 0.94], framer-motion expects it as a CubicBezier tuple: ease: [0.25, 0.46, 0.45, 0.94], }; switch (position) { case 'center': return { zIndex: 10, opacity: 1, scale: 1.1, x: 0, filter: 'grayscale(0%)', pointerEvents: 'auto', transition, }; case 'right-1': return { zIndex: 5, opacity: sideCardOpacity, scale: sideCardScale, x: cardWidth * 0.7, filter: grayscaleEffect ? 'grayscale(100%)' : 'grayscale(0%)', pointerEvents: 'auto', transition, }; case 'right-2': return { zIndex: 1, opacity: sideCardOpacity * 0.7, scale: sideCardScale * 0.9, x: cardWidth * 1.4, filter: grayscaleEffect ? 'grayscale(100%)' : 'grayscale(0%)', pointerEvents: 'auto', transition, }; case 'left-1': return { zIndex: 5, opacity: sideCardOpacity, scale: sideCardScale, x: -cardWidth * 0.7, filter: grayscaleEffect ? 'grayscale(100%)' : 'grayscale(0%)', pointerEvents: 'auto', transition, }; case 'left-2': return { zIndex: 1, opacity: sideCardOpacity * 0.7, scale: sideCardScale * 0.9, x: -cardWidth * 1.4, filter: grayscaleEffect ? 'grayscale(100%)' : 'grayscale(0%)', pointerEvents: 'auto', transition, }; default: return { zIndex: 0, opacity: 0, scale: 0.8, x: direction > 0 ? cardWidth * (visibleCards + 1) : -cardWidth * (visibleCards + 1), pointerEvents: 'none', filter: grayscaleEffect ? 'grayscale(100%)' : 'grayscale(0%)', transition, }; } }; // Auto-play functionality useEffect(() => { let interval; if (autoPlay > 0) { interval = setInterval(() => { paginate(1); }, autoPlay); } const carouselContainer = document.getElementById('team-carousel-container'); const handleMouseEnter = () => { if (pauseOnHover && autoPlay > 0) clearInterval(interval); }; const handleMouseLeave = () => { if (pauseOnHover && autoPlay > 0) { interval = setInterval(() => { paginate(1); }, autoPlay); } }; if (carouselContainer && pauseOnHover && autoPlay > 0) { carouselContainer.addEventListener('mouseenter', handleMouseEnter); carouselContainer.addEventListener('mouseleave', handleMouseLeave); } return () => { clearInterval(interval); if (carouselContainer && pauseOnHover && autoPlay > 0) { carouselContainer.removeEventListener('mouseenter', handleMouseEnter); carouselContainer.removeEventListener('mouseleave', handleMouseLeave); } }; }, [autoPlay, paginate, pauseOnHover]); // Keyboard navigation useEffect(() => { if (!keyboardNavigation) return; const handleKeyDown = (e) => { if (e.key === 'ArrowLeft') { paginate(-1); } else if (e.key === 'ArrowRight') { paginate(1); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [keyboardNavigation, paginate]); // Touch navigation const handleTouchStart = (e) => { if (!touchNavigation) return; setTouchStart(e.targetTouches[0].clientX); }; const handleTouchMove = (e) => { if (!touchNavigation) return; setTouchEnd(e.targetTouches[0].clientX); }; const handleTouchEnd = () => { if (!touchNavigation) return; const swipeThreshold = 50; const diff = touchStart - touchEnd; if (Math.abs(diff) > swipeThreshold) { if (diff > 0) { paginate(1); } else { paginate(-1); } } }; const titleSizeClasses = { sm: 'text-4xl', md: 'text-5xl', lg: 'text-6xl', xl: 'text-7xl', '2xl': 'text-8xl', }; return (_jsxs("div", { id: "team-carousel-container", className: cn(`min-h-screen flex flex-col items-center justify-center overflow-hidden relative transparent`, className), style: { background: background }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [title && (_jsx("h1", { className: cn("font-black uppercase tracking-tight absolute top-12 left-1/2 transform -translate-x-1/2 pointer-events-none whitespace-nowrap", titleSizeClasses[titleSize], titleClassName), style: { color: 'transparent', background: `linear-gradient(to bottom, ${titleColor}75 40%, transparent 76%)`, WebkitBackgroundClip: 'text', backgroundClip: 'text', }, children: title })), _jsxs("div", { className: "w-full max-w-6xl relative mt-20", style: { height: cardHeight + 100, perspective: '1000px', }, children: [showArrows && (_jsxs(_Fragment, { children: [_jsx(motion.button, { onClick: () => paginate(-1), className: "absolute left-5 top-1/2 transform -translate-y-1/2 bg-black/60 hover:bg-black/80 text-white w-10 h-10 rounded-full flex items-center justify-center z-20 transition-all duration-300 hover:scale-110", whileTap: { scale: 0.9 }, children: _jsx(ChevronLeft, { className: "w-6 h-6" }) }), _jsx(motion.button, { onClick: () => paginate(1), className: "absolute right-5 top-1/2 transform -translate-y-1/2 bg-black/60 hover:bg-black/80 text-white w-10 h-10 rounded-full flex items-center justify-center z-20 transition-all duration-300 hover:scale-110", whileTap: { scale: 0.9 }, children: _jsx(ChevronRight, { className: "w-6 h-6" }) })] })), _jsx("div", { className: "w-full h-full flex justify-center items-center relative", style: { transformStyle: 'preserve-3d' }, children: _jsx(AnimatePresence, { initial: false, custom: direction, children: members.map((member, index) => { const position = calculatePosition(index); const isCurrent = index === currentIndex; if (position === 'hidden' && !isCurrent) return null; return (_jsxs(motion.div, { className: cn("absolute bg-white overflow-hidden shadow-2xl cursor-pointer", cardClassName), style: { width: cardWidth, height: cardHeight, borderRadius: cardRadius, top: '50%', left: '50%', marginLeft: -cardWidth / 2, marginTop: -cardHeight / 2, }, initial: getVariantStyles('hidden'), animate: getVariantStyles(position), exit: getVariantStyles('hidden'), onClick: () => { if (!isCurrent) { const newDirection = index > currentIndex ? 1 : -1; setDirection(newDirection); setCurrentIndex(index); onMemberChange?.(members[index], index); } onCardClick?.(member, index); }, children: [_jsx("img", { src: member.image, alt: member.name, className: "w-full h-full object-cover" }), infoPosition === 'overlay' && (_jsxs("div", { className: "absolute bottom-0 left-0 right-0 p-4 text-center", style: { background: infoBackground || "linear-gradient(transparent, rgba(0,0,0,0.8))", color: infoTextColor, }, children: [_jsx("h3", { className: "text-lg font-bold", children: member.name }), _jsx("p", { className: "text-sm opacity-90", children: member.role })] }))] }, member.id)); }) }) })] }), infoPosition === 'bottom' && members[currentIndex] && (_jsxs(motion.div, { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 }, transition: { duration: 0.3 }, className: "text-center mt-10", children: [_jsxs("h2", { className: "text-4xl font-bold mb-3 relative inline-block", style: { color: infoTextColor }, children: [members[currentIndex].name, _jsx("span", { className: "absolute top-full left-0 w-full h-0.5 mt-2", style: { background: infoTextColor } })] }), _jsx("p", { className: "text-xl font-medium opacity-80 uppercase tracking-wider", style: { color: infoTextColor }, children: members[currentIndex].role }), members[currentIndex].bio && (_jsx("p", { className: "text-base mt-4 max-w-lg mx-auto opacity-70", children: members[currentIndex].bio }))] }, members[currentIndex].id + "-info")), showDots && (_jsx("div", { className: "flex justify-center gap-3 mt-15 ", children: members.map((_, index) => (_jsx(motion.button, { onClick: () => { if (index !== currentIndex) { const newDirection = index > currentIndex ? 1 : -1; setDirection(newDirection); setCurrentIndex(index); onMemberChange?.(members[index], index); } }, className: cn("w-3 h-3 rounded-full transition-all duration-300", index === currentIndex ? "scale-125" : "hover:scale-110"), style: { background: index === currentIndex ? infoTextColor : `${infoTextColor}40`, }, whileTap: { scale: 0.9 } }, index))) }))] })); }; export default TeamCarousel;