chore: 更新构建ID和相关文件引用
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
||||
import { FadeUp, StaggerContainer, StaggerItem, CountUp, FloatingElement, SplitText, GradientText, MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
||||
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
||||
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
||||
|
||||
@@ -62,24 +63,97 @@ export function HeroSection() {
|
||||
<section
|
||||
id="home"
|
||||
ref={sectionRef}
|
||||
className="relative min-h-screen flex items-center pt-16 overflow-hidden bg-linear-to-b from-[#F5F7FA] to-white"
|
||||
className="relative min-h-screen flex items-center pt-16 overflow-hidden bg-gradient-to-b from-[#FAFAFA] to-white"
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-200 h-150 bg-[radial-gradient(ellipse_at_center,rgba(79,70,229,0.06)_0%,transparent_60%)]" />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-[radial-gradient(ellipse_at_center,rgba(28,28,28,0.03)_0%,transparent_60%)]"
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-150 h-100 bg-[radial-gradient(ellipse_at_center,rgba(196,30,58,0.04)_0%,transparent_50%)]" />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 1.2, delay: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[600px] h-[400px] bg-[radial-gradient(ellipse_at_center,rgba(196,30,58,0.04)_0%,transparent_50%)]"
|
||||
/>
|
||||
|
||||
<FloatingElement amplitude={20} duration={6} className="absolute top-[15%] left-[5%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5, duration: 0.8 }}
|
||||
className="w-4 h-4 bg-[#C41E3A]/30 rounded-full blur-[1px]"
|
||||
/>
|
||||
</FloatingElement>
|
||||
|
||||
<FloatingElement amplitude={25} duration={7} delay={0.3} className="absolute top-[25%] right-[8%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.7, duration: 0.8 }}
|
||||
className="w-6 h-6 bg-[#1C1C1C]/20 rounded-full blur-[1px]"
|
||||
/>
|
||||
</FloatingElement>
|
||||
|
||||
<FloatingElement amplitude={18} duration={5} delay={0.6} className="absolute bottom-[30%] left-[10%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.9, duration: 0.8 }}
|
||||
className="w-3 h-3 bg-[#C41E3A]/25 rounded-full"
|
||||
/>
|
||||
</FloatingElement>
|
||||
|
||||
<FloatingElement amplitude={22} duration={6.5} delay={0.2} className="absolute bottom-[20%] right-[12%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1.1, duration: 0.8 }}
|
||||
className="w-5 h-5 bg-[#1C1C1C]/15 rounded-full blur-[1px]"
|
||||
/>
|
||||
</FloatingElement>
|
||||
|
||||
<FloatingElement amplitude={15} duration={5.5} delay={0.8} className="absolute top-[40%] left-[15%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1.3, duration: 0.8 }}
|
||||
className="w-2 h-2 bg-[#C41E3A]/40 rounded-full"
|
||||
/>
|
||||
</FloatingElement>
|
||||
|
||||
<FloatingElement amplitude={30} duration={8} delay={0.4} className="absolute top-[60%] right-[5%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1.5, duration: 0.8 }}
|
||||
className="w-8 h-8 border-2 border-[#C41E3A]/20 rounded-full"
|
||||
/>
|
||||
</FloatingElement>
|
||||
|
||||
<FloatingElement amplitude={12} duration={4.5} delay={1} className="absolute top-[35%] right-[20%]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1.7, duration: 0.8 }}
|
||||
className="w-2 h-2 bg-[#1C1C1C]/30 rounded-full"
|
||||
/>
|
||||
</FloatingElement>
|
||||
</div>
|
||||
|
||||
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6 }}
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mb-8"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
|
||||
数字印章 · 智连未来
|
||||
智连未来 · 与客户共同成长
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
@@ -89,26 +163,22 @@ export function HeroSection() {
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight mb-6"
|
||||
>
|
||||
<span className="text-[#1A1A2E]">{COMPANY_INFO.shortName}</span>
|
||||
<SplitText text={COMPANY_INFO.shortName} className="text-[#1A1A2E]" delay={0.2} />
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.15 }}
|
||||
className="text-xl sm:text-2xl text-[#4A5568] mb-4"
|
||||
>
|
||||
企业数字化转型服务商
|
||||
</motion.p>
|
||||
<BlurReveal delay={0.3}>
|
||||
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
|
||||
<GradientText colors={['#C41E3A', '#E04A68', '#C41E3A']} duration={4}>
|
||||
企业数字化转型服务商
|
||||
</GradientText>
|
||||
</p>
|
||||
</BlurReveal>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="text-lg text-[#718096] mb-10 max-w-2xl mx-auto leading-relaxed"
|
||||
>
|
||||
融合金融科技专业品质与中国传统美学,为您打造卓越的数字体验
|
||||
</motion.p>
|
||||
<BlurReveal delay={0.4}>
|
||||
<p className="text-lg text-[#718096] mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
融合金融科技专业品质与中国传统美学,为您打造卓越的数字体验
|
||||
</p>
|
||||
</BlurReveal>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -116,22 +186,26 @@ export function HeroSection() {
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => handleScrollTo('contact')}
|
||||
className="min-w-45"
|
||||
>
|
||||
立即咨询
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={() => handleScrollTo('about')}
|
||||
className="min-w-45"
|
||||
>
|
||||
了解更多
|
||||
</Button>
|
||||
<MagneticButton strength={0.4}>
|
||||
<SealButton
|
||||
size="lg"
|
||||
onClick={() => handleScrollTo('contact')}
|
||||
className="min-w-[180px]"
|
||||
>
|
||||
立即咨询
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</SealButton>
|
||||
</MagneticButton>
|
||||
<MagneticButton strength={0.4}>
|
||||
<RippleButton
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={() => handleScrollTo('about')}
|
||||
className="min-w-[180px]"
|
||||
>
|
||||
了解更多
|
||||
</RippleButton>
|
||||
</MagneticButton>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
@@ -141,13 +215,17 @@ export function HeroSection() {
|
||||
className="flex flex-wrap gap-4 justify-center mb-16"
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md"
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
|
||||
transition={{ duration: 0.4, delay: 0.4 + index * 0.1 }}
|
||||
whileHover={{ scale: 1.05, y: -2 }}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
|
||||
>
|
||||
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
|
||||
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
@@ -186,13 +264,19 @@ function StatItem({ stat, index, shouldAnimate }: {
|
||||
return (
|
||||
<motion.div
|
||||
className="group cursor-default text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={shouldAnimate ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
initial={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||
transition={{ duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
||||
whileHover={{ scale: 1.05, y: -5 }}
|
||||
>
|
||||
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
||||
{shouldAnimate ? (
|
||||
<AnimatedCounter value={numericValue} suffix={suffix} />
|
||||
<CounterWithEffect
|
||||
end={numericValue}
|
||||
suffix={suffix}
|
||||
effect="bounce"
|
||||
duration={2000}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[#CBD5E0]">0{suffix}</span>
|
||||
)}
|
||||
@@ -203,28 +287,3 @@ function StatItem({ stat, index, shouldAnimate }: {
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function AnimatedCounter({ value, suffix }: { value: number; suffix: string }) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const duration = 2000;
|
||||
const steps = 60;
|
||||
const increment = value / steps;
|
||||
let current = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= value) {
|
||||
setCount(value);
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
setCount(Math.floor(current));
|
||||
}
|
||||
}, duration / steps);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [value]);
|
||||
|
||||
return <>{count}{suffix}</>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
'use client';
|
||||
|
||||
import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
|
||||
import { useRef, useState, type ReactNode, type MouseEvent } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const cardVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
scale: 0.95,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface InkCardProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
hoverScale?: number;
|
||||
hoverRotate?: number;
|
||||
inkColor?: string;
|
||||
showInkOnHover?: boolean;
|
||||
}
|
||||
|
||||
export function InkCard({
|
||||
children,
|
||||
className = '',
|
||||
hoverScale = 1.02,
|
||||
hoverRotate = 0,
|
||||
inkColor = 'rgba(196, 30, 58, 0.05)',
|
||||
showInkOnHover = true,
|
||||
...props
|
||||
}: InkCardProps) {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const [inkPosition, setInkPosition] = useState({ x: 50, y: 50 });
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
setInkPosition({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={cardRef}
|
||||
variants={cardVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
whileHover={{
|
||||
scale: hoverScale,
|
||||
rotate: hoverRotate,
|
||||
y: -4,
|
||||
}}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
onMouseMove={handleMouseMove}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 20,
|
||||
}}
|
||||
className={cn(
|
||||
'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl transition-shadow duration-300',
|
||||
'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{showInkOnHover && (
|
||||
<motion.div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isHovered ? 1 : 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div
|
||||
className="absolute w-[200%] h-[200%] rounded-full"
|
||||
style={{
|
||||
background: `radial-gradient(circle, ${inkColor} 0%, transparent 70%)`,
|
||||
left: `${inkPosition.x - 100}%`,
|
||||
top: `${inkPosition.y - 100}%`,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
<div className="relative z-10">{children}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface GeometricCardProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
cornerColor?: string;
|
||||
}
|
||||
|
||||
export function GeometricCard({
|
||||
children,
|
||||
className = '',
|
||||
cornerColor = '#C41E3A',
|
||||
...props
|
||||
}: GeometricCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={cardVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
whileHover={{
|
||||
y: -4,
|
||||
transition: { type: 'spring', stiffness: 300, damping: 20 },
|
||||
}}
|
||||
className={cn(
|
||||
'relative bg-white border border-[#E5E5E5] rounded-xl p-6 transition-all duration-300',
|
||||
'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]',
|
||||
'group',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 rounded-tl-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
||||
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 rounded-tr-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
||||
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 rounded-bl-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 border-b-2 border-r-2 rounded-br-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300" style={{ borderColor: cornerColor }} />
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FlipCardProps {
|
||||
front: ReactNode;
|
||||
back: ReactNode;
|
||||
className?: string;
|
||||
frontClassName?: string;
|
||||
backClassName?: string;
|
||||
}
|
||||
|
||||
export function FlipCard({
|
||||
front,
|
||||
back,
|
||||
className = '',
|
||||
frontClassName = '',
|
||||
backClassName = '',
|
||||
}: FlipCardProps) {
|
||||
const [isFlipped, setIsFlipped] = useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={cn('relative cursor-pointer perspective-1000', className)}
|
||||
onClick={() => setIsFlipped(!isFlipped)}
|
||||
style={{ perspective: 1000 }}
|
||||
>
|
||||
<motion.div
|
||||
className="relative w-full h-full"
|
||||
initial={false}
|
||||
animate={{ rotateY: isFlipped ? 180 : 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={{ transformStyle: 'preserve-3d' }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 backface-hidden',
|
||||
frontClassName
|
||||
)}
|
||||
style={{ backfaceVisibility: 'hidden' }}
|
||||
>
|
||||
{front}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 backface-hidden',
|
||||
backClassName
|
||||
)}
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
transform: 'rotateY(180deg)',
|
||||
}}
|
||||
>
|
||||
{back}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TiltCardProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
maxTilt?: number;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export function TiltCard({
|
||||
children,
|
||||
className = '',
|
||||
maxTilt = 10,
|
||||
scale = 1.02,
|
||||
...props
|
||||
}: TiltCardProps) {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const [rotateX, setRotateX] = useState(0);
|
||||
const [rotateY, setRotateY] = useState(0);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
const mouseX = e.clientX - centerX;
|
||||
const mouseY = e.clientY - centerY;
|
||||
|
||||
const rotateXValue = (mouseY / (rect.height / 2)) * -maxTilt;
|
||||
const rotateYValue = (mouseX / (rect.width / 2)) * maxTilt;
|
||||
|
||||
setRotateX(rotateXValue);
|
||||
setRotateY(rotateYValue);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setRotateX(0);
|
||||
setRotateY(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={cardRef}
|
||||
className={cn('relative', className)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ transformStyle: 'preserve-3d' }}
|
||||
animate={{
|
||||
rotateX,
|
||||
rotateY,
|
||||
scale: rotateX || rotateY ? scale : 1,
|
||||
}}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface GlowCardProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
glowColor?: string;
|
||||
}
|
||||
|
||||
export function GlowCard({
|
||||
children,
|
||||
className = '',
|
||||
glowColor = 'rgba(196, 30, 58, 0.15)',
|
||||
...props
|
||||
}: GlowCardProps) {
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
const [glowPosition, setGlowPosition] = useState({ x: 50, y: 50 });
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!cardRef.current) return;
|
||||
const rect = cardRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
setGlowPosition({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={cardRef}
|
||||
variants={cardVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
whileHover={{ y: -4 }}
|
||||
onMouseMove={handleMouseMove}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
className={cn(
|
||||
'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl',
|
||||
'transition-shadow duration-300',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 pointer-events-none opacity-0 hover:opacity-100 transition-opacity duration-300"
|
||||
style={{
|
||||
background: `radial-gradient(circle at ${glowPosition.x}% ${glowPosition.y}%, ${glowColor} 0%, transparent 50%)`,
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10">{children}</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExpandCardProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
expandedContent?: ReactNode;
|
||||
}
|
||||
|
||||
export function ExpandCard({
|
||||
children,
|
||||
className = '',
|
||||
expandedContent,
|
||||
...props
|
||||
}: ExpandCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={cardVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
whileHover={{ y: -4 }}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
className={cn(
|
||||
'relative overflow-hidden bg-white border border-[#E5E5E5] rounded-xl cursor-pointer',
|
||||
'transition-shadow duration-300',
|
||||
'hover:shadow-[0_12px_24px_rgba(28,28,28,0.08)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="p-6">{children}</div>
|
||||
{expandedContent && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{
|
||||
height: isExpanded ? 'auto' : 0,
|
||||
opacity: isExpanded ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="px-6 pb-6 border-t border-[#E5E5E5] pt-4">
|
||||
{expandedContent}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
'use client';
|
||||
|
||||
import { motion, AnimatePresence, type Variants, type Transition } from 'framer-motion';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
const defaultTransition: Transition = {
|
||||
duration: 0.4,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
};
|
||||
|
||||
export const fadeInVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
transition: defaultTransition,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: { ...defaultTransition, duration: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
export const slideUpVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: defaultTransition,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
transition: { ...defaultTransition, duration: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
export const slideRightVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
x: -20,
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: defaultTransition,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
x: 20,
|
||||
transition: { ...defaultTransition, duration: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
export const scaleVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
scale: 0.98,
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: defaultTransition,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
scale: 1.02,
|
||||
transition: { ...defaultTransition, duration: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
export const inkRevealPageVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
clipPath: 'circle(0% at 50% 50%)',
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
clipPath: 'circle(100% at 50% 50%)',
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
clipPath: 'circle(0% at 50% 50%)',
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const curtainVariants: Variants = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: '100%',
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: '-100%',
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface PageTransitionProps {
|
||||
children: ReactNode;
|
||||
variants?: Variants;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageTransition({
|
||||
children,
|
||||
variants = fadeInVariants,
|
||||
className = '',
|
||||
}: PageTransitionProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={pathname}
|
||||
initial="initial"
|
||||
animate="enter"
|
||||
exit="exit"
|
||||
variants={variants}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
interface FadeTransitionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FadeTransition({ children, className = '' }: FadeTransitionProps) {
|
||||
return (
|
||||
<PageTransition variants={fadeInVariants} className={className}>
|
||||
{children}
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
interface SlideUpTransitionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SlideUpTransition({ children, className = '' }: SlideUpTransitionProps) {
|
||||
return (
|
||||
<PageTransition variants={slideUpVariants} className={className}>
|
||||
{children}
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
interface InkRevealTransitionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InkRevealTransition({ children, className = '' }: InkRevealTransitionProps) {
|
||||
return (
|
||||
<PageTransition variants={inkRevealPageVariants} className={className}>
|
||||
{children}
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
interface SectionTransitionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function SectionTransition({ children, className = '', delay = 0 }: SectionTransitionProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
delay,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StaggerChildrenProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
staggerDelay?: number;
|
||||
}
|
||||
|
||||
export function StaggerChildren({ children, className = '', staggerDelay = 0.1 }: StaggerChildrenProps) {
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: staggerDelay,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
variants={containerVariants}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface AnimatedSectionProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
animation?: 'fade' | 'slide' | 'scale' | 'ink';
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function AnimatedSection({
|
||||
children,
|
||||
className = '',
|
||||
animation = 'fade',
|
||||
delay = 0,
|
||||
}: AnimatedSectionProps) {
|
||||
const getVariants = (): Variants => {
|
||||
switch (animation) {
|
||||
case 'slide':
|
||||
return {
|
||||
hidden: { opacity: 0, y: 40 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
delay,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'scale':
|
||||
return {
|
||||
hidden: { opacity: 0, scale: 0.95 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
delay,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'ink':
|
||||
return {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
scale: 0.9,
|
||||
filter: 'blur(10px)',
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
filter: 'blur(0px)',
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
delay,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
delay,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
variants={getVariants()}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LoadingTransitionProps {
|
||||
isLoading: boolean;
|
||||
children: ReactNode;
|
||||
loadingComponent?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LoadingTransition({
|
||||
isLoading,
|
||||
children,
|
||||
loadingComponent,
|
||||
className = '',
|
||||
}: LoadingTransitionProps) {
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{isLoading ? (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={className}
|
||||
>
|
||||
{loadingComponent || (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="w-8 h-8 border-2 border-[#C41E3A] border-t-transparent rounded-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="content"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
interface SkeletonLoaderProps {
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export function SkeletonLoader({ className = '', animate = true }: SkeletonLoaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
bg-[#F5F5F5] rounded-lg
|
||||
${animate ? 'animate-pulse' : ''}
|
||||
${className}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardSkeletonProps {
|
||||
count?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardSkeleton({ count = 1, className = '' }: CardSkeletonProps) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div key={index} className="bg-white border border-[#E5E5E5] rounded-xl p-6 space-y-4">
|
||||
<SkeletonLoader className="h-4 w-3/4" />
|
||||
<SkeletonLoader className="h-4 w-1/2" />
|
||||
<SkeletonLoader className="h-20 w-full" />
|
||||
<div className="flex gap-2">
|
||||
<SkeletonLoader className="h-8 w-20" />
|
||||
<SkeletonLoader className="h-8 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { motion, AnimatePresence, type HTMLMotionProps } from 'framer-motion';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const rippleButtonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-300 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[#1C1C1C] focus-visible:ring-offset-2 focus-visible:ring-offset-white relative overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-[#C41E3A] text-white hover:bg-[#A01830] hover:shadow-[0_8px_20px_rgba(196,30,58,0.35)]',
|
||||
secondary:
|
||||
'bg-[#1C1C1C] text-white hover:bg-[#0A0A0A] hover:shadow-[0_8px_20px_rgba(28,28,28,0.35)]',
|
||||
destructive:
|
||||
'bg-[#C41E3A] text-white hover:bg-[#A01830] focus-visible:ring-[#C41E3A]',
|
||||
outline:
|
||||
'border-2 border-[#1C1C1C] bg-transparent text-[#1C1C1C] hover:bg-[#F5F5F5] hover:shadow-[0_4px_12px_rgba(28,28,28,0.2)]',
|
||||
ghost:
|
||||
'text-[#3D3D3D] hover:bg-[#F5F5F5] hover:text-[#1C1C1C]',
|
||||
link:
|
||||
'text-[#1C1C1C] underline-offset-4 hover:underline hover:text-[#C41E3A]',
|
||||
seal:
|
||||
'bg-[#C41E3A] text-white font-semibold hover:bg-[#A01830] shadow-[0_6px_20px_rgba(196,30,58,0.3)]',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-12 rounded-lg px-6 text-base',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface Ripple {
|
||||
x: number;
|
||||
y: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface RippleButtonProps
|
||||
extends VariantProps<typeof rippleButtonVariants> {
|
||||
rippleColor?: string;
|
||||
rippleDuration?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
|
||||
({ className, variant, size, rippleColor, rippleDuration = 800, onClick, children, disabled, ...props }, ref) => {
|
||||
const [ripples, setRipples] = React.useState<Ripple[]>([]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return;
|
||||
|
||||
const button = e.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const id = Date.now();
|
||||
|
||||
setRipples((prev) => [...prev, { x, y, id }]);
|
||||
|
||||
setTimeout(() => {
|
||||
setRipples((prev) => prev.filter((r) => r.id !== id));
|
||||
}, rippleDuration);
|
||||
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
const getRippleColor = () => {
|
||||
if (rippleColor) return rippleColor;
|
||||
if (variant === 'outline' || variant === 'ghost' || variant === 'link') {
|
||||
return 'rgba(196, 30, 58, 0.2)';
|
||||
}
|
||||
return 'rgba(255, 255, 255, 0.4)';
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
ref={ref}
|
||||
whileHover={disabled ? {} : { scale: 1.03, y: -3 }}
|
||||
whileTap={disabled ? {} : { scale: 0.97 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||
className={cn(rippleButtonVariants({ variant, size, className }))}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<AnimatePresence>
|
||||
{ripples.map((ripple) => (
|
||||
<motion.span
|
||||
key={ripple.id}
|
||||
initial={{ scale: 0, opacity: 0.8 }}
|
||||
animate={{ scale: 5, opacity: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: rippleDuration / 1000, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="absolute w-6 h-6 rounded-full pointer-events-none"
|
||||
style={{
|
||||
left: ripple.x - 12,
|
||||
top: ripple.y - 12,
|
||||
backgroundColor: getRippleColor(),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
);
|
||||
RippleButton.displayName = 'RippleButton';
|
||||
|
||||
export interface SealButtonProps extends VariantProps<typeof rippleButtonVariants> {
|
||||
children?: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SealButton = React.forwardRef<HTMLButtonElement, SealButtonProps>(
|
||||
({ className, variant = 'seal', size, onClick, children, disabled, ...props }, ref) => {
|
||||
const [isPressed, setIsPressed] = React.useState(false);
|
||||
const [showInk, setShowInk] = React.useState(false);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return;
|
||||
setIsPressed(true);
|
||||
setShowInk(true);
|
||||
setTimeout(() => setIsPressed(false), 600);
|
||||
setTimeout(() => setShowInk(false), 800);
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
ref={ref}
|
||||
initial={{ scale: 1, rotate: 0 }}
|
||||
whileHover={disabled ? {} : { scale: 1.06, y: -2 }}
|
||||
whileTap={disabled ? {} : { scale: 0.94, rotate: -3 }}
|
||||
animate={
|
||||
isPressed
|
||||
? {
|
||||
scale: [1, 1.15, 0.95, 1.02, 1],
|
||||
rotate: [0, -8, 8, -3, 0],
|
||||
}
|
||||
: {}
|
||||
}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 12,
|
||||
}}
|
||||
className={cn(
|
||||
rippleButtonVariants({ variant, size, className }),
|
||||
'seal-stamp'
|
||||
)}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{showInk && (
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0.6 }}
|
||||
animate={{ scale: 3, opacity: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="absolute inset-0 flex items-center justify-center pointer-events-none"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-white/20" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<span className="relative z-10 inline-flex items-center gap-1">{children}</span>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
);
|
||||
SealButton.displayName = 'SealButton';
|
||||
|
||||
export { RippleButton, SealButton, rippleButtonVariants };
|
||||
@@ -0,0 +1,438 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useScroll, useTransform, useSpring, type MotionValue, type Variants, type HTMLMotionProps } from 'framer-motion';
|
||||
import { useRef, type ReactNode } from 'react';
|
||||
|
||||
export const scrollRevealVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
scale: 0.95,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const inkRevealVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
filter: 'blur(10px)',
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
filter: 'blur(0px)',
|
||||
transition: {
|
||||
duration: 1,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const slideInLeftVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
x: -50,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const slideInRightVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
x: 50,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface ScrollRevealProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
variants?: Variants;
|
||||
once?: boolean;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export function ScrollReveal({
|
||||
children,
|
||||
className = '',
|
||||
delay = 0,
|
||||
variants = scrollRevealVariants,
|
||||
once = true,
|
||||
threshold = 0.1,
|
||||
...props
|
||||
}: ScrollRevealProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start end', 'start center'],
|
||||
});
|
||||
|
||||
const opacity = useTransform(scrollYProgress, [0, threshold], [0, 1]);
|
||||
const y = useTransform(scrollYProgress, [0, threshold], [50, 0]);
|
||||
const scale = useTransform(scrollYProgress, [0, threshold], [0.95, 1]);
|
||||
|
||||
const smoothOpacity = useSpring(opacity, { stiffness: 100, damping: 30 });
|
||||
const smoothY = useSpring(y, { stiffness: 100, damping: 30 });
|
||||
const smoothScale = useSpring(scale, { stiffness: 100, damping: 30 });
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
style={{
|
||||
opacity: smoothOpacity,
|
||||
y: smoothY,
|
||||
scale: smoothScale,
|
||||
}}
|
||||
initial={{ opacity: 0, y: 50, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
||||
viewport={{ once, amount: threshold }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ParallaxSectionProps extends HTMLMotionProps<'section'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export function ParallaxSection({
|
||||
children,
|
||||
className = '',
|
||||
speed = 0.5,
|
||||
...props
|
||||
}: ParallaxSectionProps) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start end', 'end start'],
|
||||
});
|
||||
|
||||
const y = useTransform(scrollYProgress, [0, 1], ['0%', `${speed * 100}%`]);
|
||||
const smoothY = useSpring(y, { stiffness: 100, damping: 30 });
|
||||
|
||||
return (
|
||||
<motion.section ref={ref} style={{ y: smoothY }} className={className} {...props}>
|
||||
{children}
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
|
||||
interface ParallaxImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export function ParallaxImage({ src, alt, className = '', speed = 0.3 }: ParallaxImageProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start end', 'end start'],
|
||||
});
|
||||
|
||||
const y = useTransform(scrollYProgress, [0, 1], [`${-speed * 50}%`, `${speed * 50}%`]);
|
||||
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1.1, 1, 1.1]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`overflow-hidden ${className}`}>
|
||||
<motion.img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{ y, scale }}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ScaleOnScrollProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
minScale?: number;
|
||||
maxScale?: number;
|
||||
}
|
||||
|
||||
export function ScaleOnScroll({
|
||||
children,
|
||||
className = '',
|
||||
minScale = 0.8,
|
||||
maxScale = 1,
|
||||
...props
|
||||
}: ScaleOnScrollProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start end', 'center center'],
|
||||
});
|
||||
|
||||
const scale = useTransform(scrollYProgress, [0, 1], [minScale, maxScale]);
|
||||
const smoothScale = useSpring(scale, { stiffness: 100, damping: 30 });
|
||||
|
||||
return (
|
||||
<motion.div ref={ref} style={{ scale: smoothScale }} className={className} {...props}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FadeOnScrollProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
direction?: 'up' | 'down' | 'left' | 'right';
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
export function FadeOnScroll({
|
||||
children,
|
||||
className = '',
|
||||
direction = 'up',
|
||||
distance = 50,
|
||||
...props
|
||||
}: FadeOnScrollProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start end', 'center center'],
|
||||
});
|
||||
|
||||
const getTransform = () => {
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
case 'down':
|
||||
return useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
case 'left':
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
case 'right':
|
||||
return useTransform(scrollYProgress, [0, 1], [-distance, 0]);
|
||||
default:
|
||||
return useTransform(scrollYProgress, [0, 1], [distance, 0]);
|
||||
}
|
||||
};
|
||||
|
||||
const transform = getTransform();
|
||||
const opacity = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
|
||||
|
||||
const smoothTransform = useSpring(transform, { stiffness: 100, damping: 30 });
|
||||
const smoothOpacity = useSpring(opacity, { stiffness: 100, damping: 30 });
|
||||
|
||||
const style =
|
||||
direction === 'left' || direction === 'right' ? { x: smoothTransform, opacity: smoothOpacity } : { y: smoothTransform, opacity: smoothOpacity };
|
||||
|
||||
return (
|
||||
<motion.div ref={ref} style={style} className={className} {...props}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StaggerRevealProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
staggerDelay?: number;
|
||||
containerDelay?: number;
|
||||
}
|
||||
|
||||
export function StaggerReveal({
|
||||
children,
|
||||
className = '',
|
||||
staggerDelay = 0.1,
|
||||
containerDelay = 0,
|
||||
...props
|
||||
}: StaggerRevealProps) {
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: staggerDelay,
|
||||
delayChildren: containerDelay,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.2 }}
|
||||
variants={containerVariants}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StaggerItemProps extends HTMLMotionProps<'div'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StaggerItem({ children, className = '', ...props }: StaggerItemProps) {
|
||||
const itemVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
scale: 0.95,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div variants={itemVariants} className={className} {...props}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TextRevealProps extends HTMLMotionProps<'div'> {
|
||||
text: string;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function TextReveal({ text, className = '', delay = 0, ...props }: TextRevealProps) {
|
||||
const words = text.split(' ');
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
delayChildren: delay,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wordVariants: Variants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={containerVariants}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{words.map((word, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
variants={wordVariants}
|
||||
className="inline-block mr-2"
|
||||
>
|
||||
{word}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgressIndicatorProps {
|
||||
className?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function ProgressIndicator({ className = '', color = '#C41E3A' }: ProgressIndicatorProps) {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30 });
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`fixed top-0 left-0 right-0 h-1 origin-left z-50 ${className}`}
|
||||
style={{ scaleX, backgroundColor: color }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ScrollTriggeredCounterProps {
|
||||
end: number;
|
||||
duration?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ScrollTriggeredCounter({
|
||||
end,
|
||||
duration = 2,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
className = '',
|
||||
}: ScrollTriggeredCounterProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start end', 'center center'],
|
||||
});
|
||||
|
||||
const displayNumber = useTransform(scrollYProgress, [0, 1], [0, end]);
|
||||
const rounded = useTransform(displayNumber, (latest) => Math.round(latest));
|
||||
|
||||
return (
|
||||
<motion.span ref={ref} className={className}>
|
||||
{prefix}
|
||||
<motion.span>{rounded}</motion.span>
|
||||
{suffix}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user