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
+268
View File
@@ -681,6 +681,144 @@
}
}
@keyframes ripple {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(4);
opacity: 0;
}
}
@keyframes inkDrop {
0% {
transform: scale(0);
opacity: 0;
filter: blur(8px);
}
50% {
opacity: 0.8;
filter: blur(2px);
}
100% {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}
@keyframes brushStroke {
0% {
stroke-dashoffset: 1000;
opacity: 0;
}
50% {
opacity: 1;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
@keyframes sealPress {
0% {
transform: scale(1.8) rotate(-20deg);
opacity: 0;
}
60% {
transform: scale(0.9) rotate(2deg);
opacity: 1;
}
80% {
transform: scale(1.05) rotate(-1deg);
}
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
@keyframes inkBleed {
0% {
box-shadow: 0 0 0 0 rgba(28, 28, 28, 0.1);
}
100% {
box-shadow: 0 0 20px 10px rgba(28, 28, 28, 0);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes breathe {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
@keyframes slideUp {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideDown {
0% {
transform: translateY(-100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes expandWidth {
0% {
width: 0;
opacity: 0;
}
100% {
width: 100%;
opacity: 1;
}
}
@keyframes typewriter {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0;
}
}
.animate-glow {
animation: glow 2s ease-in-out infinite;
}
@@ -693,6 +831,136 @@
animation: pulse-glow 2s ease-in-out infinite;
}
.animate-ripple {
animation: ripple 0.6s ease-out forwards;
}
.animate-ink-drop {
animation: inkDrop 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-seal-press {
animation: sealPress 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.animate-ink-bleed {
animation: inkBleed 1s ease-out forwards;
}
.animate-shimmer {
background: linear-gradient(
90deg,
rgba(196, 30, 58, 0) 0%,
rgba(196, 30, 58, 0.1) 50%,
rgba(196, 30, 58, 0) 100%
);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.animate-breathe {
animation: breathe 4s ease-in-out infinite;
}
.animate-slide-up {
animation: slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-slide-down {
animation: slideDown 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-expand-width {
animation: expandWidth 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.hover-lift {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(28, 28, 28, 0.1);
}
.hover-scale {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.hover-scale:hover {
transform: scale(1.02);
}
.hover-glow {
transition: box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.hover-glow:hover {
box-shadow: 0 0 20px rgba(196, 30, 58, 0.3);
}
.hover-ink {
position: relative;
overflow: hidden;
}
.hover-ink::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(28, 28, 28, 0.05);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1), height 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.hover-ink:hover::before {
width: 300%;
height: 300%;
}
.ink-border {
position: relative;
}
.ink-border::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, #1C1C1C, #C41E3A);
transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.ink-border:hover::after {
width: 100%;
}
.seal-stamp {
position: relative;
}
.seal-stamp::before {
content: '';
position: absolute;
inset: -4px;
border: 2px solid rgba(196, 30, 58, 0.3);
border-radius: inherit;
opacity: 0;
transform: scale(1.1);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.seal-stamp:hover::before {
opacity: 1;
transform: scale(1);
}
.primary-gradient {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
}
+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>
);
}
+946
View File
@@ -0,0 +1,946 @@
'use client';
import { motion, useInView, useSpring, useTransform, type MotionValue, type Variants, type HTMLMotionProps } from 'framer-motion';
import { useRef, useEffect, useState, type ReactNode } from 'react';
export const inkVariants: Variants = {
hidden: {
opacity: 0,
scale: 0.8,
filter: 'blur(10px)',
},
visible: {
opacity: 1,
scale: 1,
filter: 'blur(0px)',
transition: {
duration: 0.8,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const sealStampVariants: Variants = {
hidden: {
opacity: 0,
scale: 1.5,
rotate: -15,
},
visible: {
opacity: 1,
scale: 1,
rotate: 0,
transition: {
type: 'spring',
stiffness: 300,
damping: 20,
mass: 1,
},
},
};
export const brushStrokeVariants: Variants = {
hidden: {
pathLength: 0,
opacity: 0,
},
visible: {
pathLength: 1,
opacity: 1,
transition: {
pathLength: {
duration: 1.5,
ease: 'easeInOut',
},
opacity: {
duration: 0.3,
},
},
},
};
export const fadeUpVariants: Variants = {
hidden: {
opacity: 0,
y: 30,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const staggerContainerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.1,
},
},
};
export const staggerItemVariants: Variants = {
hidden: {
opacity: 0,
y: 20,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: [0.16, 1, 0.3, 1],
},
},
};
interface InkRevealProps extends HTMLMotionProps<'div'> {
children: ReactNode;
delay?: number;
className?: string;
}
export function InkReveal({ children, delay = 0, className = '', ...props }: InkRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={inkVariants}
transition={{ delay }}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface SealStampProps extends HTMLMotionProps<'div'> {
children: ReactNode;
delay?: number;
className?: string;
}
export function SealStamp({ children, delay = 0, className = '', ...props }: SealStampProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={sealStampVariants}
transition={{ delay }}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface FadeUpProps extends HTMLMotionProps<'div'> {
children: ReactNode;
delay?: number;
duration?: number;
className?: string;
}
export function FadeUp({ children, delay = 0, duration = 0.6, className = '', ...props }: FadeUpProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={{
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration,
delay,
ease: [0.16, 1, 0.3, 1],
},
},
}}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface StaggerContainerProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
staggerDelay?: number;
}
export function StaggerContainer({ children, className = '', staggerDelay = 0.1, ...props }: StaggerContainerProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: staggerDelay,
delayChildren: 0.1,
},
},
}}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface StaggerItemProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
}
export function StaggerItem({ children, className = '', ...props }: StaggerItemProps) {
return (
<motion.div
variants={staggerItemVariants}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface RippleButtonProps {
children: ReactNode;
onClick?: () => void;
className?: string;
rippleColor?: string;
}
export function RippleButton({ children, onClick, className = '', rippleColor = 'rgba(196, 30, 58, 0.3)' }: RippleButtonProps) {
const [ripples, setRipples] = useState<Array<{ x: number; y: number; id: number }>>([]);
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
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));
}, 600);
onClick?.();
};
return (
<motion.button
onClick={handleClick}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className={`relative overflow-hidden ${className}`}
>
{children}
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className="absolute w-4 h-4 rounded-full pointer-events-none"
style={{
left: ripple.x - 8,
top: ripple.y - 8,
backgroundColor: rippleColor,
}}
/>
))}
</motion.button>
);
}
interface InkCardProps {
children: ReactNode;
className?: string;
hoverScale?: number;
hoverShadow?: string;
}
export function InkCard({ children, className = '', hoverScale = 1.02, hoverShadow = '0 20px 40px rgba(28, 28, 28, 0.1)' }: InkCardProps) {
return (
<motion.div
whileHover={{
scale: hoverScale,
boxShadow: hoverShadow,
y: -4,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 20,
}}
className={className}
>
{children}
</motion.div>
);
}
export function useParallax(value: MotionValue<number>, distance: number) {
return useTransform(value, [0, 1], [-distance, distance]);
}
export function useSmoothSpring(value: MotionValue<number>, springConfig = { stiffness: 100, damping: 30, restDelta: 0.001 }) {
return useSpring(value, springConfig);
}
interface CountUpProps {
end: number;
duration?: number;
delay?: number;
prefix?: string;
suffix?: string;
className?: string;
}
export function CountUp({ end, duration = 2000, delay = 0, prefix = '', suffix = '', className = '' }: CountUpProps) {
const [count, setCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const hasAnimated = useRef(false);
useEffect(() => {
if (!isInView || hasAnimated.current) return;
hasAnimated.current = true;
const startTime = Date.now() + delay;
const endTime = startTime + duration;
const animate = () => {
const now = Date.now();
if (now < startTime) {
requestAnimationFrame(animate);
return;
}
if (now >= endTime) {
setCount(end);
return;
}
const progress = (now - startTime) / duration;
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = Math.floor(easeOutQuart * end);
setCount(currentValue);
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [isInView, end, duration, delay]);
return (
<motion.span
ref={ref}
initial={{ opacity: 0, scale: 0.5 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.5, delay: delay / 1000 }}
className={className}
>
{prefix}
{count}
{suffix}
</motion.span>
);
}
interface TypewriterProps {
text: string;
delay?: number;
speed?: number;
className?: string;
cursorClassName?: string;
}
export function Typewriter({ text, delay = 0, speed = 50, className = '', cursorClassName = '' }: TypewriterProps) {
const [displayText, setDisplayText] = useState('');
const [isTyping, setIsTyping] = useState(false);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
useEffect(() => {
if (!isInView) return;
const startTyping = setTimeout(() => {
setIsTyping(true);
let currentIndex = 0;
const typeInterval = setInterval(() => {
if (currentIndex < text.length) {
setDisplayText(text.slice(0, currentIndex + 1));
currentIndex++;
} else {
clearInterval(typeInterval);
setIsTyping(false);
}
}, speed);
return () => clearInterval(typeInterval);
}, delay);
return () => clearTimeout(startTyping);
}, [isInView, text, delay, speed]);
return (
<span ref={ref} className={className}>
{displayText}
<motion.span
animate={{ opacity: [1, 0] }}
transition={{ duration: 0.5, repeat: Infinity, repeatType: 'reverse' }}
className={cursorClassName}
style={{ marginLeft: '2px' }}
>
|
</motion.span>
</span>
);
}
interface FloatingElementProps {
children: ReactNode;
amplitude?: number;
duration?: number;
delay?: number;
className?: string;
}
export function FloatingElement({ children, amplitude = 10, duration = 3, delay = 0, className = '' }: FloatingElementProps) {
return (
<motion.div
animate={{
y: [-amplitude, amplitude, -amplitude],
}}
transition={{
duration,
delay,
repeat: Infinity,
ease: 'easeInOut',
}}
className={className}
>
{children}
</motion.div>
);
}
interface PulseElementProps {
children: ReactNode;
scale?: number;
duration?: number;
className?: string;
}
export function PulseElement({ children, scale = 1.05, duration = 2, className = '' }: PulseElementProps) {
return (
<motion.div
animate={{
scale: [1, scale, 1],
}}
transition={{
duration,
repeat: Infinity,
ease: 'easeInOut',
}}
className={className}
>
{children}
</motion.div>
);
}
export function InkDropSVG({ className = '' }: { className?: string }) {
return (
<motion.svg
viewBox="0 0 100 100"
className={className}
initial="hidden"
animate="visible"
>
<motion.circle
cx="50"
cy="50"
r="40"
fill="currentColor"
variants={{
hidden: { scale: 0, opacity: 0 },
visible: {
scale: [0, 1.2, 1],
opacity: [0, 1, 1],
transition: {
duration: 0.8,
ease: [0.16, 1, 0.3, 1],
},
},
}}
/>
</motion.svg>
);
}
interface GradientTextProps {
children: ReactNode;
className?: string;
colors?: string[];
duration?: number;
}
export function GradientText({
children,
className = '',
colors = ['#C41E3A', '#E04A68', '#C41E3A'],
duration = 3
}: GradientTextProps) {
return (
<motion.span
className={className}
style={{
background: `linear-gradient(90deg, ${colors.join(', ')})`,
backgroundSize: '200% 100%',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
animate={{
backgroundPosition: ['0% 50%', '100% 50%', '0% 50%'],
}}
transition={{
duration,
repeat: Infinity,
ease: 'linear',
}}
>
{children}
</motion.span>
);
}
interface SplitTextProps {
text: string;
className?: string;
delay?: number;
staggerDelay?: number;
}
export function SplitText({ text, className = '', delay = 0, staggerDelay = 0.03 }: SplitTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: staggerDelay,
delayChildren: delay,
},
},
};
const child: Variants = {
hidden: {
opacity: 0,
y: 20,
rotateX: -90,
},
visible: {
opacity: 1,
y: 0,
rotateX: 0,
transition: {
type: 'spring' as const,
stiffness: 100,
damping: 12,
},
},
};
return (
<motion.span
ref={ref}
variants={container}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
className={className}
style={{ display: 'inline-block' }}
>
{text.split('').map((char, index) => (
<motion.span
key={index}
variants={child}
style={{ display: 'inline-block' }}
>
{char === ' ' ? '\u00A0' : char}
</motion.span>
))}
</motion.span>
);
}
interface GlitchTextProps {
text: string;
className?: string;
}
export function GlitchText({ text, className = '' }: GlitchTextProps) {
return (
<span className={`relative ${className}`}>
<span className="relative z-10">{text}</span>
<motion.span
className="absolute top-0 left-0 text-[#C41E3A] z-0"
animate={{ x: [-2, 2, -2], opacity: [0.8, 0.4, 0.8] }}
transition={{ duration: 0.3, repeat: Infinity }}
>
{text}
</motion.span>
<motion.span
className="absolute top-0 left-0 text-[#1C1C1C] z-0"
animate={{ x: [2, -2, 2], opacity: [0.8, 0.4, 0.8] }}
transition={{ duration: 0.3, repeat: Infinity, delay: 0.15 }}
>
{text}
</motion.span>
</span>
);
}
interface MagneticButtonProps {
children: ReactNode;
className?: string;
strength?: number;
onClick?: () => void;
}
export function MagneticButton({ children, className = '', strength = 0.3, onClick }: MagneticButtonProps) {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const x = (e.clientX - centerX) * strength;
const y = (e.clientY - centerY) * strength;
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
return (
<motion.div
ref={ref}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
animate={{ x: position.x, y: position.y }}
transition={{ type: 'spring', stiffness: 150, damping: 15, mass: 0.1 }}
className={className}
onClick={onClick}
style={{ display: 'inline-block' }}
>
{children}
</motion.div>
);
}
interface BlurRevealProps {
children: ReactNode;
className?: string;
delay?: number;
}
export function BlurReveal({ children, className = '', delay = 0 }: BlurRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial={{ filter: 'blur(20px)', opacity: 0, scale: 1.1 }}
animate={isInView ? { filter: 'blur(0px)', opacity: 1, scale: 1 } : {}}
transition={{ duration: 1, delay, ease: [0.16, 1, 0.3, 1] }}
className={className}
>
{children}
</motion.div>
);
}
interface WaveTextProps {
text: string;
className?: string;
delay?: number;
}
export function WaveText({ text, className = '', delay = 0 }: WaveTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: delay,
},
},
};
const child: Variants = {
hidden: { y: 0 },
visible: {
y: [0, -10, 0],
transition: {
duration: 0.5,
ease: 'easeInOut' as const,
},
},
};
return (
<motion.span
ref={ref}
variants={container}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
className={className}
style={{ display: 'inline-block' }}
>
{text.split('').map((char, index) => (
<motion.span
key={index}
variants={child}
style={{ display: 'inline-block' }}
transition={{ delay: index * 0.05 }}
>
{char === ' ' ? '\u00A0' : char}
</motion.span>
))}
</motion.span>
);
}
interface RotatingBorderProps {
children: ReactNode;
className?: string;
borderWidth?: number;
duration?: number;
colors?: string[];
}
export function RotatingBorder({
children,
className = '',
borderWidth = 2,
duration = 4,
colors = ['#C41E3A', '#1C1C1C', '#C41E3A']
}: RotatingBorderProps) {
const gradient = `conic-gradient(from 0deg, ${colors.join(', ')})`;
return (
<div className={`relative p-[${borderWidth}px] rounded-lg overflow-hidden ${className}`}>
<motion.div
className="absolute inset-0"
style={{ background: gradient }}
animate={{ rotate: 360 }}
transition={{ duration, repeat: Infinity, ease: 'linear' }}
/>
<div className="relative bg-white rounded-md">
{children}
</div>
</div>
);
}
interface ShimmerButtonProps {
children: ReactNode;
className?: string;
shimmerColor?: string;
onClick?: () => void;
}
export function ShimmerButton({ children, className = '', shimmerColor = 'rgba(255,255,255,0.3)', onClick }: ShimmerButtonProps) {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={`relative overflow-hidden ${className}`}
onClick={onClick}
>
<motion.div
className="absolute inset-0"
style={{
background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
backgroundSize: '200% 100%',
}}
animate={{
backgroundPosition: ['-200% 0', '200% 0'],
}}
transition={{
duration: 1.5,
repeat: Infinity,
repeatDelay: 2,
ease: 'linear',
}}
/>
<span className="relative z-10">{children}</span>
</motion.button>
);
}
interface InkSplashProps {
className?: string;
color?: string;
size?: number;
}
export function InkSplash({ className = '', color = '#C41E3A', size = 100 }: InkSplashProps) {
const pathVariants: Variants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
pathLength: 1,
opacity: 1,
transition: { duration: 1.5, ease: [0.16, 1, 0.3, 1] as const },
},
};
return (
<motion.svg
viewBox="0 0 100 100"
className={className}
width={size}
height={size}
initial="hidden"
animate="visible"
>
<motion.path
d="M50 10 C30 20 20 40 25 60 C30 80 50 90 50 90 C50 90 70 80 75 60 C80 40 70 20 50 10"
fill={color}
variants={pathVariants}
/>
</motion.svg>
);
}
interface CounterWithEffectProps {
end: number;
duration?: number;
prefix?: string;
suffix?: string;
className?: string;
effect?: 'bounce' | 'slide' | 'flip';
}
export function CounterWithEffect({
end,
duration = 2000,
prefix = '',
suffix = '',
className = '',
effect = 'bounce'
}: CounterWithEffectProps) {
const [count, setCount] = useState(0);
const [prevCount, setPrevCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const hasAnimated = useRef(false);
useEffect(() => {
if (!isInView || hasAnimated.current) return;
hasAnimated.current = true;
const startTime = Date.now();
const animate = () => {
const progress = Math.min((Date.now() - startTime) / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const newCount = Math.floor(easeOutQuart * end);
if (newCount !== count) {
setPrevCount(count);
setCount(newCount);
}
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}, [isInView, end, duration, count]);
const effectVariants = {
bounce: {
initial: { y: 0 },
animate: { y: [0, -10, 0] },
transition: { duration: 0.3 },
},
slide: {
initial: { y: 20, opacity: 0 },
animate: { y: 0, opacity: 1 },
transition: { duration: 0.2 },
},
flip: {
initial: { rotateX: 90 },
animate: { rotateX: 0 },
transition: { duration: 0.2 },
},
};
return (
<motion.span
ref={ref}
key={count}
initial={effectVariants[effect].initial}
animate={effectVariants[effect].animate}
transition={effectVariants[effect].transition}
className={className}
>
{prefix}{count}{suffix}
</motion.span>
);
}