93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
|
export function useStaggerReveal(count: number, baseDelay = 80) {
|
|
const [visible, setVisible] = useState<Set<number>>(new Set());
|
|
|
|
useEffect(() => {
|
|
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
for (let i = 0; i < count; i++) {
|
|
timers.push(
|
|
setTimeout(
|
|
() => setVisible((prev) => new Set(prev).add(i)),
|
|
baseDelay * (i + 1) + 200
|
|
)
|
|
);
|
|
}
|
|
return () => timers.forEach(clearTimeout);
|
|
}, [count, baseDelay]);
|
|
|
|
return visible;
|
|
}
|
|
|
|
export function useMountTransition(delay = 50) {
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const t = setTimeout(() => setMounted(true), delay);
|
|
return () => clearTimeout(t);
|
|
}, [delay]);
|
|
|
|
return mounted;
|
|
}
|
|
|
|
|
|
export function useWiggle(durationMs = 1500, onComplete: () => void) {
|
|
const [wiggling, setWiggling] = useState(false);
|
|
const [progress, setProgress] = useState(0);
|
|
|
|
const startTime = useRef<number>(0);
|
|
const rafId = useRef<number>(0);
|
|
const completeTimer = useRef<ReturnType<typeof setTimeout>>(null);
|
|
const completedRef = useRef(false);
|
|
|
|
const stop = useCallback(() => {
|
|
setWiggling(false);
|
|
setProgress(0);
|
|
cancelAnimationFrame(rafId.current);
|
|
if (completeTimer.current) clearTimeout(completeTimer.current);
|
|
}, []);
|
|
|
|
const tick = useCallback(() => {
|
|
const elapsed = Date.now() - startTime.current;
|
|
const p = Math.min(1, elapsed / durationMs);
|
|
setProgress(p);
|
|
if (p < 1) {
|
|
rafId.current = requestAnimationFrame(tick);
|
|
}
|
|
}, [durationMs]);
|
|
|
|
const start = useCallback(() => {
|
|
completedRef.current = false;
|
|
startTime.current = Date.now();
|
|
setWiggling(true);
|
|
setProgress(0);
|
|
rafId.current = requestAnimationFrame(tick);
|
|
|
|
completeTimer.current = setTimeout(() => {
|
|
completedRef.current = true;
|
|
stop();
|
|
onComplete();
|
|
}, durationMs);
|
|
}, [durationMs, onComplete, tick, stop]);
|
|
|
|
const release = useCallback(() => {
|
|
if (!completedRef.current) stop();
|
|
}, [stop]);
|
|
|
|
// cleanup on unmount
|
|
useEffect(() => () => {
|
|
cancelAnimationFrame(rafId.current);
|
|
if (completeTimer.current) clearTimeout(completeTimer.current);
|
|
}, []);
|
|
|
|
const handlers = {
|
|
onPointerDown: start,
|
|
onPointerEnter: start,
|
|
onPointerUp: release,
|
|
onPointerLeave: release,
|
|
};
|
|
|
|
return { wiggling, progress, handlers };
|
|
} |