Files
whispering-tree/components/ui/page-transition.tsx

158 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

2025-11-10 17:10:34 +05:30
"use client";
import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap";
interface PageTransitionProps {
isOpen: boolean;
onComplete?: () => void;
direction?: "reveal" | "unreveal";
}
/* ------------------------------------------------------------------
1. Path data identical to the original demo
------------------------------------------------------------------ */
const paths = {
step1: {
unfilled: "M 0 100 V 100 Q 50 100 100 100 V 100 z",
inBetween: {
curve1: "M 0 100 V 50 Q 50 0 100 50 V 100 z",
curve2: "M 0 100 V 50 Q 50 100 100 50 V 100 z",
},
filled: "M 0 100 V 0 Q 50 0 100 0 V 100 z",
},
step2: {
filled: "M 0 0 V 100 Q 50 100 100 100 V 0 z",
inBetween: {
curve1: "M 0 0 V 50 Q 50 0 100 50 V 0 z",
curve2: "M 0 0 V 50 Q 50 100 100 50 V 0 z",
},
unfilled: "M 0 0 V 0 Q 50 0 100 0 V 0 z",
},
};
export const PageTransition = ({
isOpen,
onComplete,
direction = "reveal",
}: PageTransitionProps) => {
const pathRef = useRef<SVGPathElement>(null);
const svgRef = useRef<SVGSVGElement>(null); // 👈 new ref for hiding
const [animating, setAnimating] = useState(false);
/* --------------------------------------------------------------
REVEAL open second view
-------------------------------------------------------------- */
const reveal = () => {
if (animating) return;
setAnimating(true);
// 👇 Show overlay before animation starts
gsap.set(svgRef.current, { display: "block" });
const tl = gsap.timeline({
onComplete: () => {
setAnimating(false);
onComplete?.();
// 👇 Hide overlay after animation completes
gsap.set(svgRef.current, { display: "none" });
},
});
tl.set(pathRef.current, { attr: { d: paths.step1.unfilled } })
.to(pathRef.current, {
duration: 0.8,
ease: "power4.in",
attr: { d: paths.step1.inBetween.curve1 },
})
.to(pathRef.current, {
duration: 0.2,
ease: "power1",
attr: { d: paths.step1.filled },
onComplete: () => onComplete?.(),
})
.set(pathRef.current, { attr: { d: paths.step2.filled } })
.to(pathRef.current, {
duration: 0.2,
ease: "sine.in",
attr: { d: paths.step2.inBetween.curve1 },
})
.to(pathRef.current, {
duration: 1,
ease: "power4",
attr: { d: paths.step2.unfilled },
});
};
/* --------------------------------------------------------------
UNREVEAL back to first view
-------------------------------------------------------------- */
const unreveal = () => {
if (animating) return;
setAnimating(true);
// 👇 Show overlay before animation starts
gsap.set(svgRef.current, { display: "block" });
const tl = gsap.timeline({
onComplete: () => {
setAnimating(false);
onComplete?.();
// 👇 Hide overlay after animation completes
gsap.set(svgRef.current, { display: "none" });
},
});
tl.set(pathRef.current, { attr: { d: paths.step2.unfilled } })
.to(pathRef.current, {
duration: 0.8,
ease: "power4.in",
attr: { d: paths.step2.inBetween.curve2 },
})
.to(pathRef.current, {
duration: 0.2,
ease: "power1",
attr: { d: paths.step2.filled },
onComplete: () => onComplete?.(),
})
.set(pathRef.current, { attr: { d: paths.step1.filled } })
.to(pathRef.current, {
duration: 0.2,
ease: "sine.in",
attr: { d: paths.step1.inBetween.curve2 },
})
.to(pathRef.current, {
duration: 1,
ease: "power4",
attr: { d: paths.step1.unfilled },
});
};
/* --------------------------------------------------------------
React to prop changes
-------------------------------------------------------------- */
useEffect(() => {
if (!isOpen) return;
if (direction === "reveal") reveal();
else if (direction === "unreveal") unreveal();
}, [isOpen, direction]);
return (
<svg
ref={svgRef}
className="pointer-events-none fixed top-0 left-0 z-[9999] h-screen w-screen"
viewBox="0 0 100 100"
preserveAspectRatio="none"
style={{ display: "none" }} // 👈 hidden by default
>
<path
ref={pathRef}
className="overlay__path"
d="M 0 100 V 100 Q 50 100 100 100 V 100 z"
vectorEffect="non-scaling-stroke"
fill="#274b2d"
/>
</svg>
);
};