chore: 更新构建ID和相关文件引用
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user