318 lines
7.4 KiB
TypeScript
318 lines
7.4 KiB
TypeScript
'use client'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { motion, useInView, useAnimation } from 'framer-motion'
|
|
|
|
// Scroll-triggered animation hook
|
|
export function useScrollAnimation(threshold = 0.1, once = true) {
|
|
const ref = useRef(null)
|
|
const isInView = useInView(ref, { threshold, once })
|
|
const controls = useAnimation()
|
|
|
|
useEffect(() => {
|
|
if (isInView) {
|
|
controls.start('visible')
|
|
}
|
|
}, [isInView, controls])
|
|
|
|
return { ref, controls }
|
|
}
|
|
|
|
// Fade up animation component
|
|
export function FadeUp({ children, delay = 0, className = '' }: {
|
|
children: React.ReactNode
|
|
delay?: number
|
|
className?: string
|
|
}) {
|
|
const { ref, controls } = useScrollAnimation()
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
animate={controls}
|
|
initial="hidden"
|
|
variants={{
|
|
hidden: { opacity: 0, y: 40 },
|
|
visible: { opacity: 1, y: 0 }
|
|
}}
|
|
transition={{ duration: 0.6, delay, ease: "easeOut" }}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Stagger children animation
|
|
export function StaggerContainer({ children, className = '', staggerDelay = 0.1 }: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
staggerDelay?: number
|
|
}) {
|
|
const { ref, controls } = useScrollAnimation()
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
animate={controls}
|
|
initial="hidden"
|
|
variants={{
|
|
hidden: {},
|
|
visible: {
|
|
transition: {
|
|
staggerChildren: staggerDelay
|
|
}
|
|
}
|
|
}}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Individual stagger item
|
|
export function StaggerItem({ children, className = '' }: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<motion.div
|
|
variants={{
|
|
hidden: { opacity: 0, y: 30 },
|
|
visible: { opacity: 1, y: 0 }
|
|
}}
|
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Scale in animation
|
|
export function ScaleIn({ children, delay = 0, className = '' }: {
|
|
children: React.ReactNode
|
|
delay?: number
|
|
className?: string
|
|
}) {
|
|
const { ref, controls } = useScrollAnimation()
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
animate={controls}
|
|
initial="hidden"
|
|
variants={{
|
|
hidden: { opacity: 0, scale: 0.8 },
|
|
visible: { opacity: 1, scale: 1 }
|
|
}}
|
|
transition={{ duration: 0.6, delay, ease: "backOut" }}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Slide in from left
|
|
export function SlideInLeft({ children, delay = 0, className = '' }: {
|
|
children: React.ReactNode
|
|
delay?: number
|
|
className?: string
|
|
}) {
|
|
const { ref, controls } = useScrollAnimation()
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
animate={controls}
|
|
initial="hidden"
|
|
variants={{
|
|
hidden: { opacity: 0, x: -50 },
|
|
visible: { opacity: 1, x: 0 }
|
|
}}
|
|
transition={{ duration: 0.6, delay, ease: "easeOut" }}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Slide in from right
|
|
export function SlideInRight({ children, delay = 0, className = '' }: {
|
|
children: React.ReactNode
|
|
delay?: number
|
|
className?: string
|
|
}) {
|
|
const { ref, controls } = useScrollAnimation()
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
animate={controls}
|
|
initial="hidden"
|
|
variants={{
|
|
hidden: { opacity: 0, x: 50 },
|
|
visible: { opacity: 1, x: 0 }
|
|
}}
|
|
transition={{ duration: 0.6, delay, ease: "easeOut" }}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Hover card with micro-interactions
|
|
export function HoverCard({ children, className = '' }: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<motion.div
|
|
whileHover={{
|
|
y: -8,
|
|
scale: 1.02,
|
|
transition: { duration: 0.2, ease: "easeOut" }
|
|
}}
|
|
whileTap={{ scale: 0.98 }}
|
|
className={className}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Magnetic button effect
|
|
export function MagneticButton({ children, className = '', strength = 0.3 }: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
strength?: number
|
|
}) {
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
if (!ref.current) return
|
|
const rect = ref.current.getBoundingClientRect()
|
|
const x = e.clientX - rect.left - rect.width / 2
|
|
const y = e.clientY - rect.top - rect.height / 2
|
|
|
|
ref.current.style.transform = `translate(${x * strength}px, ${y * strength}px)`
|
|
}
|
|
|
|
const handleMouseLeave = () => {
|
|
if (!ref.current) return
|
|
ref.current.style.transform = 'translate(0px, 0px)'
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseLeave={handleMouseLeave}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
|
className={className}
|
|
style={{ transition: 'transform 0.3s cubic-bezier(0.23, 1, 0.320, 1)' }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)
|
|
}
|
|
|
|
// Counter animation
|
|
export function CountUp({ end, duration = 2, suffix = '' }: {
|
|
end: number
|
|
duration?: number
|
|
suffix?: string
|
|
}) {
|
|
const ref = useRef(null)
|
|
const isInView = useInView(ref, { threshold: 0.1, once: true })
|
|
const [count, setCount] = useState(0)
|
|
|
|
useEffect(() => {
|
|
if (!isInView) return
|
|
|
|
let startTime: number
|
|
let animationFrame: number
|
|
|
|
const animate = (timestamp: number) => {
|
|
if (!startTime) startTime = timestamp
|
|
const progress = Math.min((timestamp - startTime) / (duration * 1000), 1)
|
|
|
|
setCount(Math.floor(progress * end))
|
|
|
|
if (progress < 1) {
|
|
animationFrame = requestAnimationFrame(animate)
|
|
}
|
|
}
|
|
|
|
animationFrame = requestAnimationFrame(animate)
|
|
|
|
return () => {
|
|
if (animationFrame) {
|
|
cancelAnimationFrame(animationFrame)
|
|
}
|
|
}
|
|
}, [isInView, end, duration])
|
|
|
|
return (
|
|
<motion.span
|
|
ref={ref}
|
|
initial={{ opacity: 0 }}
|
|
animate={isInView ? { opacity: 1 } : { opacity: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
{count}{suffix}
|
|
</motion.span>
|
|
)
|
|
}
|
|
|
|
// Parallax effect
|
|
export function ParallaxElement({ children, speed = 0.5, className = '' }: {
|
|
children: React.ReactNode
|
|
speed?: number
|
|
className?: string
|
|
}) {
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
const element = ref.current
|
|
if (!element) return
|
|
|
|
const handleScroll = () => {
|
|
const scrolled = window.pageYOffset
|
|
const rect = element.getBoundingClientRect()
|
|
const elementTop = rect.top + scrolled
|
|
const elementHeight = rect.height
|
|
const windowHeight = window.innerHeight
|
|
|
|
if (scrolled + windowHeight > elementTop && scrolled < elementTop + elementHeight) {
|
|
const yPos = -(scrolled - elementTop) * speed
|
|
element.style.transform = `translateY(${yPos}px)`
|
|
}
|
|
}
|
|
|
|
let ticking = false
|
|
const throttledScroll = () => {
|
|
if (!ticking) {
|
|
requestAnimationFrame(() => {
|
|
handleScroll()
|
|
ticking = false
|
|
})
|
|
ticking = true
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', throttledScroll, { passive: true })
|
|
return () => window.removeEventListener('scroll', throttledScroll)
|
|
}, [speed])
|
|
|
|
return (
|
|
<div ref={ref} className={className} style={{ willChange: 'transform' }}>
|
|
{children}
|
|
</div>
|
|
)
|
|
} |