chore: 更新构建ID和相关文件引用

This commit is contained in:
张翔
2026-02-23 10:38:29 +08:00
parent b38fd022b6
commit 25586a5a6c
152 changed files with 3230 additions and 571 deletions
+134 -75
View File
@@ -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}</>;
}
+356
View File
@@ -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>
);
}
+421
View File
@@ -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>
);
}
+191
View File
@@ -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 };
+438
View File
@@ -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>
);
}