← back to Watches
src/components/LuxuryInteractions.jsx
472 lines
/**
* LUXURY MICRO-INTERACTIONS LIBRARY
* Premium interaction components for the Omega Watch platform
*
* Features:
* - Haptic feedback simulation
* - Sound effects (optional)
* - Premium animations
* - Gesture controls
* - Parallax effects
*/
import React, { useState, useEffect, useRef } from 'react';
import { motion, useMotionValue, useTransform, useSpring } from 'framer-motion';
// ============================================================================
// HAPTIC FEEDBACK SIMULATOR
// ============================================================================
export const useHaptic = () => {
const triggerHaptic = (type = 'light') => {
// Web Vibration API (if supported)
if ('vibrate' in navigator) {
const patterns = {
light: [10],
medium: [15],
heavy: [20],
success: [10, 50, 10],
error: [20, 100, 20],
selection: [5],
};
navigator.vibrate(patterns[type] || patterns.light);
}
// Visual feedback as fallback
document.body.classList.add('haptic-feedback');
setTimeout(() => document.body.classList.remove('haptic-feedback'), 100);
};
return { triggerHaptic };
};
// ============================================================================
// LUXURY BUTTON with premium interactions
// ============================================================================
export const LuxuryButton = ({
children,
onClick,
variant = 'primary',
size = 'md',
icon,
loading = false,
disabled = false,
haptic = true,
className = ''
}) => {
const { triggerHaptic } = useHaptic();
const [isPressed, setIsPressed] = useState(false);
const variants = {
primary: 'bg-gradient-to-r from-omega-red to-red-700 text-white shadow-lg hover:shadow-redGlow',
secondary: 'bg-gradient-to-r from-omega-navy to-gray-800 text-white shadow-md hover:shadow-lg',
gold: 'bg-gradient-to-r from-yellow-400 to-yellow-600 text-omega-navy shadow-md hover:shadow-goldGlow',
outline: 'border-2 border-omega-red text-omega-red hover:bg-omega-red hover:text-white',
ghost: 'text-omega-red hover:bg-red-50 dark:hover:bg-red-900/20',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-6 py-3 text-base',
lg: 'px-8 py-4 text-lg',
xl: 'px-10 py-5 text-xl',
};
const handleClick = (e) => {
if (disabled || loading) return;
if (haptic) triggerHaptic('light');
setIsPressed(true);
setTimeout(() => setIsPressed(false), 150);
if (onClick) onClick(e);
};
return (
<motion.button
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98, y: 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
onClick={handleClick}
disabled={disabled || loading}
className={`
${variants[variant]}
${sizes[size]}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
${isPressed ? 'ring-4 ring-omega-red/30' : ''}
font-semibold rounded-xl transition-all duration-300
flex items-center justify-center space-x-2
relative overflow-hidden
${className}
`}
>
{/* Shimmer effect on hover */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
initial={{ x: '-100%' }}
whileHover={{ x: '100%' }}
transition={{ duration: 0.6, ease: 'easeInOut' }}
/>
{loading ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
className="w-5 h-5 border-2 border-white border-t-transparent rounded-full"
/>
) : (
<>
{icon && <span>{icon}</span>}
<span className="relative z-10">{children}</span>
</>
)}
</motion.button>
);
};
// ============================================================================
// PARALLAX CONTAINER
// ============================================================================
export const ParallaxContainer = ({ children, intensity = 10 }) => {
const ref = useRef(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(y, [-100, 100], [intensity, -intensity]);
const rotateY = useTransform(x, [-100, 100], [-intensity, intensity]);
const handleMouse = (event) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
x.set(event.clientX - centerX);
y.set(event.clientY - centerY);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
return (
<motion.div
ref={ref}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
style={{
rotateX,
rotateY,
transformStyle: 'preserve-3d',
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
{children}
</motion.div>
);
};
// ============================================================================
// MAGNETIC BUTTON - Follows cursor on hover
// ============================================================================
export const MagneticButton = ({ children, className = '', onClick }) => {
const ref = useRef(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const springConfig = { damping: 15, stiffness: 150 };
const springX = useSpring(x, springConfig);
const springY = useSpring(y, springConfig);
const handleMouseMove = (event) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distanceX = event.clientX - centerX;
const distanceY = event.clientY - centerY;
x.set(distanceX * 0.3);
y.set(distanceY * 0.3);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
return (
<motion.button
ref={ref}
style={{ x: springX, y: springY }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={onClick}
className={className}
>
{children}
</motion.button>
);
};
// ============================================================================
// LUXURY CARD with 3D tilt effect
// ============================================================================
export const LuxuryCard = ({ children, className = '', onClick, glowColor = 'rgba(200, 16, 46, 0.3)' }) => {
const ref = useRef(null);
const [isHovered, setIsHovered] = useState(false);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(y, [-100, 100], [10, -10]);
const rotateY = useTransform(x, [-100, 100], [-10, 10]);
const handleMouse = (event) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
x.set(event.clientX - centerX);
y.set(event.clientY - centerY);
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
setIsHovered(false);
};
return (
<motion.div
ref={ref}
onMouseMove={handleMouse}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={handleMouseLeave}
onClick={onClick}
style={{
rotateX,
rotateY,
transformStyle: 'preserve-3d',
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
className={`relative ${className}`}
>
{/* Glow effect on hover */}
<motion.div
className="absolute inset-0 rounded-xl blur-xl"
style={{
background: glowColor,
opacity: isHovered ? 1 : 0,
}}
transition={{ duration: 0.3 }}
/>
{/* Card content */}
<div className="relative z-10">
{children}
</div>
</motion.div>
);
};
// ============================================================================
// ANIMATED NUMBER COUNTER
// ============================================================================
export const AnimatedNumber = ({ value, prefix = '', suffix = '', duration = 1000, decimals = 0 }) => {
const [displayValue, setDisplayValue] = useState(0);
useEffect(() => {
let startTime;
let animationFrame;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const current = value * easeOutQuart;
setDisplayValue(current);
if (progress < 1) {
animationFrame = requestAnimationFrame(animate);
}
};
animationFrame = requestAnimationFrame(animate);
return () => {
if (animationFrame) cancelAnimationFrame(animationFrame);
};
}, [value, duration]);
return (
<span>
{prefix}{displayValue.toFixed(decimals).toLocaleString()}{suffix}
</span>
);
};
// ============================================================================
// SHIMMER LOADING SKELETON
// ============================================================================
export const ShimmerSkeleton = ({ width = '100%', height = '20px', className = '' }) => {
return (
<div
className={`relative overflow-hidden rounded-lg bg-gray-200 dark:bg-gray-700 ${className}`}
style={{ width, height }}
>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{
repeat: Infinity,
duration: 1.5,
ease: 'linear',
}}
/>
</div>
);
};
// ============================================================================
// PREMIUM TOOLTIP
// ============================================================================
export const PremiumTooltip = ({ children, content, position = 'top' }) => {
const [isVisible, setIsVisible] = useState(false);
const positions = {
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
};
return (
<div
className="relative inline-block"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{
opacity: isVisible ? 1 : 0,
scale: isVisible ? 1 : 0.9,
}}
transition={{ duration: 0.2 }}
className={`
absolute ${positions[position]}
px-3 py-2 bg-omega-navy text-white text-sm rounded-lg
shadow-xl whitespace-nowrap z-50
pointer-events-none
`}
style={{ display: isVisible ? 'block' : 'none' }}
>
{content}
{/* Arrow */}
<div className="absolute w-2 h-2 bg-omega-navy transform rotate-45"
style={{
...(position === 'top' && { bottom: '-4px', left: '50%', marginLeft: '-4px' }),
...(position === 'bottom' && { top: '-4px', left: '50%', marginLeft: '-4px' }),
...(position === 'left' && { right: '-4px', top: '50%', marginTop: '-4px' }),
...(position === 'right' && { left: '-4px', top: '50%', marginTop: '-4px' }),
}}
/>
</motion.div>
</div>
);
};
// ============================================================================
// CINEMATIC REVEAL ANIMATION
// ============================================================================
export const CinematicReveal = ({ children, delay = 0 }) => {
return (
<motion.div
initial={{ opacity: 0, y: 50, rotateX: -15 }}
animate={{ opacity: 1, y: 0, rotateX: 0 }}
transition={{
duration: 0.8,
delay,
ease: [0.25, 0.46, 0.45, 0.94], // Luxury easing
}}
>
{children}
</motion.div>
);
};
// ============================================================================
// RIPPLE EFFECT (Material Design inspired, luxury version)
// ============================================================================
export const RippleEffect = ({ children, className = '', onClick }) => {
const [ripples, setRipples] = useState([]);
const addRipple = (event) => {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
const newRipple = { x, y, size, id: Date.now() };
setRipples((prev) => [...prev, newRipple]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
}, 600);
if (onClick) onClick(event);
};
return (
<button
className={`relative overflow-hidden ${className}`}
onClick={addRipple}
>
{children}
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
className="absolute bg-white/30 rounded-full pointer-events-none"
style={{
left: ripple.x,
top: ripple.y,
width: ripple.size,
height: ripple.size,
}}
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
/>
))}
</button>
);
};
export default {
useHaptic,
LuxuryButton,
ParallaxContainer,
MagneticButton,
LuxuryCard,
AnimatedNumber,
ShimmerSkeleton,
PremiumTooltip,
CinematicReveal,
RippleEffect,
};