michaelpeskov/components/ScrollAnimations.tsx

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>
)
}