50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
|
|
import { useEffect, useRef, useState } from "react";
|
||
|
|
import { SLOT_WIDTH } from "~/components/scoring/BracketTreeView";
|
||
|
|
|
||
|
|
interface AnimState {
|
||
|
|
fromPage: number;
|
||
|
|
toPage: number;
|
||
|
|
dir: "left" | "right";
|
||
|
|
phase: "sliding" | "settling";
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useRoundTransition(maxPage: number, defaultPage: number) {
|
||
|
|
const [page, setPage] = useState(defaultPage);
|
||
|
|
const [anim, setAnim] = useState<AnimState | null>(null);
|
||
|
|
const stripRef = useRef<HTMLDivElement>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!anim || anim.phase !== "sliding" || !stripRef.current) return;
|
||
|
|
const el = stripRef.current;
|
||
|
|
const finalX = anim.dir === "right" ? -SLOT_WIDTH : 0;
|
||
|
|
const raf = requestAnimationFrame(() => {
|
||
|
|
el.style.transition = "transform 200ms ease";
|
||
|
|
el.style.transform = `translateX(${finalX}px)`;
|
||
|
|
});
|
||
|
|
return () => cancelAnimationFrame(raf);
|
||
|
|
}, [anim]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!anim || anim.phase !== "settling") return;
|
||
|
|
const timer = setTimeout(() => setAnim(null), 520);
|
||
|
|
return () => clearTimeout(timer);
|
||
|
|
}, [anim]);
|
||
|
|
|
||
|
|
const navigate = (newPage: number) => {
|
||
|
|
if (anim || newPage < 0 || newPage > maxPage) return;
|
||
|
|
setAnim({ fromPage: page, toPage: newPage, dir: newPage > page ? "right" : "left", phase: "sliding" });
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleTransitionEnd = () => {
|
||
|
|
if (!anim || anim.phase !== "sliding") return;
|
||
|
|
if (stripRef.current) {
|
||
|
|
stripRef.current.style.transition = "none";
|
||
|
|
stripRef.current.style.transform = "translateX(0)";
|
||
|
|
}
|
||
|
|
setPage(anim.toPage);
|
||
|
|
setAnim((prev) => (prev ? { ...prev, phase: "settling" } : null));
|
||
|
|
};
|
||
|
|
|
||
|
|
return { page, anim, stripRef, navigate, handleTransitionEnd };
|
||
|
|
}
|