MVp
This commit is contained in:
318
components/ScrollAnimations.tsx
Normal file
318
components/ScrollAnimations.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user