Files
cfw-autumn/apps/website/components/elastic-footer.tsx
2026-04-16 12:27:04 +01:00

109 lines
3.3 KiB
TypeScript
Executable File

"use client";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import type { ReactNode } from "react";
import { useEffect } from "react";
import AnimatedFooterImage from "./animated-footer-image";
export default function ElasticRecoil({ children }: { children: ReactNode }) {
const liftAmount = useMotionValue(0);
const springConfig = { stiffness: 600, damping: 35, mass: 1 };
const animatedLift = useSpring(liftAmount, springConfig);
const y = useTransform(animatedLift, [0, 400], [0, -280]);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | null = null;
let recoilFired = false;
let isTouching = false; // Track if finger is on screen
const triggerRebound = () => {
liftAmount.set(0);
if (!recoilFired) {
recoilFired = true;
window.dispatchEvent(new CustomEvent("elastic-recoil"));
}
};
const handleWheel = (e: WheelEvent) => {
const isAtBottom =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
if (isAtBottom && e.deltaY > 0) {
liftAmount.set(liftAmount.get() + e.deltaY * 0.5);
recoilFired = false;
// Reset timeout on every wheel event
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
if (!isTouching) {
triggerRebound();
}
}, 1500); // Rebounds 1.5 seconds after wheel stops
} else if (e.deltaY < 0) {
if (!isTouching) {
liftAmount.set(0);
recoilFired = false;
}
}
};
let touchStart = 0;
const handleTouchStart = (e: TouchEvent) => {
isTouching = true;
touchStart = e.touches[0].clientY;
recoilFired = false;
if (timeout) clearTimeout(timeout);
};
const handleTouchMove = (e: TouchEvent) => {
const isAtBottom =
window.innerHeight + window.pageYOffset >=
document.documentElement.scrollHeight - 5;
if (isAtBottom) {
const touchDelta = touchStart - e.touches[0].clientY;
if (touchDelta > 0) {
liftAmount.set(touchDelta * 1.5);
recoilFired = false;
// Notice: We don't set a timeout here, so it never snaps back while touching
}
}
};
const handleTouchEnd = () => {
isTouching = false;
if (liftAmount.get() > 0) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
triggerRebound();
}, 1500); // Rebounds 1.5 seconds after lifting finger
}
};
window.addEventListener("wheel", handleWheel, { passive: true });
window.addEventListener("touchstart", handleTouchStart, { passive: true });
window.addEventListener("touchmove", handleTouchMove, { passive: true });
window.addEventListener("touchend", handleTouchEnd, { passive: true });
window.addEventListener("touchcancel", handleTouchEnd, { passive: true });
return () => {
if (timeout) clearTimeout(timeout);
window.removeEventListener("wheel", handleWheel);
window.removeEventListener("touchstart", handleTouchStart);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", handleTouchEnd);
window.removeEventListener("touchcancel", handleTouchEnd);
};
}, [liftAmount]);
return (
<div className="relative w-full overflow-hidden">
<AnimatedFooterImage />
<motion.div style={{ y }} className="relative z-10 bg-black">
{children}
</motion.div>
</div>
);
}