"use client"; import React, { useState, useCallback, useEffect } from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { motion, AnimatePresence, TargetAndTransition} from 'framer-motion'; import { cn } from '../../lib/utils'; // Assuming this utility correctly merges class names export interface TeamMember { id: string; name: string; role: string; image: string; bio?: string; } export interface TeamCarouselProps { /** Array of team members */ members: TeamMember[]; /** Title displayed above the carousel */ title?: string; /** Title font size */ titleSize?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; /** Title color */ titleColor?: string; /** Background color or gradient. Overrides the default 'bg-background' class. */ background?: string; /** Card width in pixels */ cardWidth?: number; /** Card height in pixels */ cardHeight?: number; /** Card border radius */ cardRadius?: number; /** Enable/disable navigation arrows */ showArrows?: boolean; /** Enable/disable dots indicator */ showDots?: boolean; /** Enable/disable keyboard navigation */ keyboardNavigation?: boolean; /** Enable/disable touch/swipe navigation */ touchNavigation?: boolean; /** Animation duration in milliseconds */ animationDuration?: number; /** Auto-play interval in milliseconds (0 to disable) */ autoPlay?: number; /** Pause auto-play on hover */ pauseOnHover?: boolean; /** Number of visible cards on each side */ visibleCards?: number; /** Scale factor for side cards */ sideCardScale?: number; /** Opacity for side cards */ sideCardOpacity?: number; /** Apply grayscale filter to side cards */ grayscaleEffect?: boolean; /** Custom className for container */ className?: string; /** Custom className for cards */ cardClassName?: string; /** Custom className for title */ titleClassName?: string; /** Member info position */ infoPosition?: 'bottom' | 'overlay' | 'none'; /** Info text color */ infoTextColor?: string; /** Info background */ infoBackground?: string; /** Callback when active member changes */ onMemberChange?: (member: TeamMember, index: number) => void; /** Callback when card is clicked */ onCardClick?: (member: TeamMember, index: number) => void; /** Initial active index */ initialIndex?: number; } export const TeamCarousel: React.FC = ({ 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: number) => { 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: number) => { return (index + totalMembers) % totalMembers; }; const calculatePosition = (index: number) => { 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: string): TargetAndTransition => { // 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] as [number, number, number, number], }; 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: NodeJS.Timeout; 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: KeyboardEvent) => { 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: React.TouchEvent) => { if (!touchNavigation) return; setTouchStart(e.targetTouches[0].clientX); }; const handleTouchMove = (e: React.TouchEvent) => { 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 ( ); }; export default TeamCarousel;