import { useState, useEffect, useRef, useCallback } from 'react'; import { UseCarouselOptions, UseCarouselReturn } from '../types'; const DRAG_THRESHOLD = 50; const SWIPE_THRESHOLD_RATIO = 1.2; export const useCarousel = ( totalItems: number, options: UseCarouselOptions = {} ): UseCarouselReturn => { const { itemsPerPage = 2, onPageChange } = options; const [currentPage, setCurrentPage] = useState(0); const carouselRef = useRef(null); const isScrollingRef = useRef(false); const isDraggingRef = useRef(false); const startXRef = useRef(0); const touchStartXRef = useRef(null); const touchStartYRef = useRef(null); const isSwipingRef = useRef(false); const scrollTimeoutRef = useRef(null); const totalPages = Math.ceil(totalItems / itemsPerPage); const scrollToPage = useCallback( (page: number) => { if (carouselRef.current) { const cardElements = carouselRef.current.children; if (cardElements.length > 0) { isScrollingRef.current = true; const container = carouselRef.current; const targetCardIndex = page * itemsPerPage; const targetCard = cardElements[targetCardIndex] as HTMLElement; if (targetCard) { const targetScroll = targetCard.offsetLeft; container.scrollTo({ left: targetScroll, behavior: 'smooth' }); setCurrentPage(page); if (onPageChange) { onPageChange(page); } setTimeout(() => { isScrollingRef.current = false; }, 400); } } } }, [itemsPerPage, onPageChange] ); const handlePrevious = useCallback( () => { const newPage = Math.max(0, currentPage - 1); scrollToPage(newPage); }, [currentPage, scrollToPage] ); const handleNext = useCallback( () => { const newPage = Math.min(totalPages - 1, currentPage + 1); scrollToPage(newPage); }, [currentPage, totalPages, scrollToPage] ); const handleDragNavigation = useCallback( (diff: number) => { if (Math.abs(diff) > DRAG_THRESHOLD) { if (diff > 0) { handleNext(); } else { handlePrevious(); } } }, [handleNext, handlePrevious] ); const handleMouseDown = useCallback((e: React.MouseEvent) => { if (!carouselRef.current) { return; } e.preventDefault(); isDraggingRef.current = true; startXRef.current = e.pageX; carouselRef.current.style.cursor = 'grabbing'; }, []); const handleMouseUp = useCallback( (e: React.MouseEvent) => { if (!isDraggingRef.current) { return; } isDraggingRef.current = false; if (carouselRef.current) { carouselRef.current.style.cursor = 'grab'; const endX = e.pageX; const diff = startXRef.current - endX; handleDragNavigation(diff); } }, [handleDragNavigation] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (!isDraggingRef.current) { return; } isDraggingRef.current = false; if (carouselRef.current) { carouselRef.current.style.cursor = 'grab'; const endX = e.pageX; const diff = startXRef.current - endX; handleDragNavigation(diff); } }, [handleDragNavigation] ); const handleTouchStart = (e: React.TouchEvent) => { if (!carouselRef.current) { return; } touchStartXRef.current = e.touches[0].clientX; touchStartYRef.current = e.touches[0].clientY; isSwipingRef.current = false; }; const handleTouchMove = (e: React.TouchEvent) => { if ( touchStartXRef.current === null || touchStartYRef.current === null || isSwipingRef.current ) { return; } const currentX = e.touches[0].clientX; const currentY = e.touches[0].clientY; const diffX = touchStartXRef.current - currentX; const diffY = touchStartYRef.current - currentY; if (Math.abs(diffX) > Math.abs(diffY) * SWIPE_THRESHOLD_RATIO) { if (e.cancelable) { e.preventDefault(); } } if ( Math.abs(diffX) > DRAG_THRESHOLD && Math.abs(diffX) > Math.abs(diffY) * SWIPE_THRESHOLD_RATIO ) { if (e.cancelable) { e.preventDefault(); } isSwipingRef.current = true; handleDragNavigation(diffX); } }; const handleTouchEnd = () => { touchStartXRef.current = null; touchStartYRef.current = null; isSwipingRef.current = false; }; const handleScroll = useCallback( () => { if (isScrollingRef.current) { return; } if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } scrollTimeoutRef.current = setTimeout(() => { if (carouselRef.current) { const container = carouselRef.current; const children = container.children; if (children.length === 0) { return; } const scrollLeft = container.scrollLeft; let closestPage = 0; let closestDistance = Infinity; for (let page = 0; page < totalPages; page++) { const cardIndex = page * itemsPerPage; if (cardIndex < children.length) { const card = children[cardIndex] as HTMLElement; const distance = Math.abs(card.offsetLeft - scrollLeft); if (distance < closestDistance) { closestDistance = distance; closestPage = page; } } } if (closestPage !== currentPage) { setCurrentPage(closestPage); if (onPageChange) { onPageChange(closestPage); } } } }, 50); }, [totalPages, itemsPerPage, onPageChange, currentPage] ); useEffect( () => { const carousel = carouselRef.current; if (carousel) { carousel.addEventListener('scroll', handleScroll); return () => carousel.removeEventListener('scroll', handleScroll); } return undefined; }, [handleScroll] ); return { currentPage, totalPages, carouselRef, isScrolling: isScrollingRef.current, handlePrevious, handleNext, scrollToPage, handleMouseDown, handleMouseUp, handleMouseLeave, handleTouchStart, handleTouchMove, handleTouchEnd }; };