feat: wrapup wow — animated counters, scroll-reveal, fun facts, component split, budget formatting
Some checks failed
CI / Check / Test (push) Failing after 6m25s
Some checks failed
CI / Check / Test (push) Failing after 6m25s
This commit is contained in:
57
spa/src/hooks/use-animate.ts
Normal file
57
spa/src/hooks/use-animate.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
export function useCountUp(target: number, duration = 1200) {
|
||||
const [value, setValue] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const started = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !started.current) {
|
||||
started.current = true
|
||||
const start = performance.now()
|
||||
const step = (now: number) => {
|
||||
const progress = Math.min((now - start) / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
setValue(Math.round(eased * target))
|
||||
if (progress < 1) requestAnimationFrame(step)
|
||||
}
|
||||
requestAnimationFrame(step)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [target, duration])
|
||||
|
||||
return { ref, value }
|
||||
}
|
||||
|
||||
export function useScrollReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return { ref, visible }
|
||||
}
|
||||
Reference in New Issue
Block a user