← back to Watches
src/components/GestureControls.jsx
200 lines
import React, { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
/**
* Gesture Control System
* Features: Swipe navigation, pinch zoom simulation, haptic feedback
*/
const GestureControls = ({ items = [], currentIndex = 0, onIndexChange }) => {
const [touchStart, setTouchStart] = useState(null);
const [touchEnd, setTouchEnd] = useState(null);
const [showIndicator, setShowIndicator] = useState(true);
const containerRef = useRef(null);
const minSwipeDistance = 50;
useEffect(() => {
const timer = setTimeout(() => setShowIndicator(false), 5000);
return () => clearTimeout(timer);
}, []);
const onTouchStart = (e) => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
setShowIndicator(true);
};
const onTouchMove = (e) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe && currentIndex < items.length - 1) {
onIndexChange(currentIndex + 1);
triggerHaptic();
}
if (isRightSwipe && currentIndex > 0) {
onIndexChange(currentIndex - 1);
triggerHaptic();
}
};
const triggerHaptic = () => {
// Simulate haptic feedback with visual/audio cues
if (navigator.vibrate) {
navigator.vibrate(10);
}
};
const handleKeyDown = (e) => {
if (e.key === 'ArrowLeft' && currentIndex > 0) {
onIndexChange(currentIndex - 1);
triggerHaptic();
} else if (e.key === 'ArrowRight' && currentIndex < items.length - 1) {
onIndexChange(currentIndex + 1);
triggerHaptic();
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [currentIndex, items.length]);
return (
<div
ref={containerRef}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
className="relative w-full"
>
{/* Gesture indicator */}
<AnimatePresence>
{showIndicator && items.length > 1 && (
<motion.div
className="swipe-indicator no-print"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
>
{items.map((_, index) => (
<div
key={index}
className={`swipe-dot ${index === currentIndex ? 'active' : ''}`}
/>
))}
</motion.div>
)}
</AnimatePresence>
{/* Navigation arrows for desktop */}
{items.length > 1 && (
<>
{currentIndex > 0 && (
<motion.button
className="fixed left-4 top-1/2 -translate-y-1/2 z-50 no-print hidden md:block"
onClick={() => {
onIndexChange(currentIndex - 1);
triggerHaptic();
}}
whileHover={{ scale: 1.1, x: -5 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
aria-label="Previous item"
>
<div className="glass-luxury p-4 rounded-full shadow-gold haptic-click">
<svg
className="w-6 h-6 text-gold"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</div>
</motion.button>
)}
{currentIndex < items.length - 1 && (
<motion.button
className="fixed right-4 top-1/2 -translate-y-1/2 z-50 no-print hidden md:block"
onClick={() => {
onIndexChange(currentIndex + 1);
triggerHaptic();
}}
whileHover={{ scale: 1.1, x: 5 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
aria-label="Next item"
>
<div className="glass-luxury p-4 rounded-full shadow-gold haptic-click">
<svg
className="w-6 h-6 text-gold"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</motion.button>
)}
</>
)}
{/* Counter display */}
{items.length > 1 && (
<motion.div
className="fixed top-24 right-4 z-50 no-print"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
>
<div className="glass-luxury px-4 py-2 rounded-full text-gold font-bold text-sm">
{currentIndex + 1} / {items.length}
</div>
</motion.div>
)}
{/* Keyboard navigation hint */}
<AnimatePresence>
{showIndicator && (
<motion.div
className="fixed bottom-24 left-1/2 -translate-x-1/2 z-50 no-print hidden md:block"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
>
<div className="glass-luxury px-6 py-3 rounded-full text-gray-300 text-xs tracking-wider flex items-center gap-3">
<kbd className="px-2 py-1 bg-gray-800 rounded text-gold">←</kbd>
<span>Navigate</span>
<kbd className="px-2 py-1 bg-gray-800 rounded text-gold">→</kbd>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default GestureControls;