mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
import { useEffect } from "react";
|
|
|
|
type UseScrollHintOptions = {
|
|
containerRef: HTMLDivElement | null;
|
|
enabled: boolean;
|
|
delayMs?: number;
|
|
scrollAmount?: number;
|
|
animationDuration?: number;
|
|
onHintShown?: () => void;
|
|
};
|
|
|
|
/**
|
|
* A hook that provides a gentle scroll hint animation to encourage user interaction.
|
|
* After a delay, if the user hasn't scrolled, it will scroll down slightly and bounce back.
|
|
*/
|
|
export function useScrollHint({
|
|
containerRef,
|
|
enabled,
|
|
delayMs = 2000,
|
|
scrollAmount = 80,
|
|
animationDuration = 1000,
|
|
onHintShown,
|
|
}: UseScrollHintOptions) {
|
|
useEffect(() => {
|
|
if (containerRef == null || !enabled) {
|
|
return;
|
|
}
|
|
|
|
const hintTimer = setTimeout(() => {
|
|
if (!enabled || containerRef.scrollTop !== 0) {
|
|
return;
|
|
}
|
|
|
|
const startScrollTop = containerRef.scrollTop;
|
|
const startTime = Date.now();
|
|
|
|
// Temporarily disable scroll snap for smooth animation
|
|
const originalScrollSnapType = containerRef.style.scrollSnapType;
|
|
containerRef.style.scrollSnapType = "none";
|
|
|
|
const animate = () => {
|
|
const elapsed = Date.now() - startTime;
|
|
const progress = Math.min(elapsed / animationDuration, 1);
|
|
|
|
// Bouncy easing function - overshoots and bounces back
|
|
const easeOutBounce = (t: number) => {
|
|
const n1 = 7.5625;
|
|
const d1 = 2.75;
|
|
if (t < 1 / d1) {
|
|
return n1 * t * t;
|
|
} else if (t < 2 / d1) {
|
|
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
} else if (t < 2.5 / d1) {
|
|
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
} else {
|
|
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
}
|
|
};
|
|
|
|
// Create a bounce effect: scroll down quickly, then bounce back
|
|
let offset;
|
|
if (progress < 0.4) {
|
|
// First 40%: scroll down quickly
|
|
const t = progress / 0.4;
|
|
offset = scrollAmount * t * t; // Quadratic ease-in
|
|
} else {
|
|
// Last 60%: bounce back with overshoot
|
|
const t = (progress - 0.4) / 0.6;
|
|
offset = scrollAmount * (1 - easeOutBounce(t));
|
|
}
|
|
|
|
containerRef.scrollTop = startScrollTop + offset;
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(animate);
|
|
} else {
|
|
// Ensure we end exactly where we started
|
|
containerRef.scrollTop = startScrollTop;
|
|
// Re-enable scroll snap
|
|
containerRef.style.scrollSnapType = originalScrollSnapType;
|
|
}
|
|
};
|
|
|
|
animate();
|
|
onHintShown?.();
|
|
}, delayMs);
|
|
|
|
return () => {
|
|
clearTimeout(hintTimer);
|
|
};
|
|
}, [
|
|
containerRef,
|
|
enabled,
|
|
delayMs,
|
|
scrollAmount,
|
|
animationDuration,
|
|
onHintShown,
|
|
]);
|
|
}
|