"use client";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { gsap, EASE } from "@/lib/gsap";
import { WolfMark } from "@/components/brand/WolfMark";
import { chrome as chromeDe, type Chrome } from "@/content/chrome";
import { crossesLangRoot } from "@/lib/i18n";
import { endTransition, setTransitionPhase, subscribe } from "./transition-store";
export interface CurtainProps {
/** The monogram (48 px). layout.tsx passes ``; falls back to ``. */
children?: ReactNode;
/** Texte der Hülle. Ohne Angabe die deutsche Fassung. */
chrome?: Chrome;
}
/**
* The page-turn sheet: fixed ivory, 1 px gold hairline on its top edge,
* monogram + destination label centred. Note the start state lives ONLY in
* GSAP (`yPercent`) – a CSS `translateY(100%)` would be read as the tween's
* base `y` and added on top, which pushed the whole cover tween one viewport
* below the fold (the reader saw a hard cut, then the sheet wiping up over the
* new page). Slides up 0.45 s (power3.inOut),
* pushes the route, keeps rising once the pathname changed, then resets.
* Hard fallback: full navigation when the route has not changed after 900 ms.
*/
export function Curtain({ children, chrome = chromeDe }: CurtainProps) {
const router = useRouter();
const pathname = usePathname();
const sheet = useRef(null);
const phase = useRef<"idle" | "cover" | "pushed" | "reveal">("idle");
const timer = useRef(0);
const [label, setLabel] = useState(null);
useEffect(() => {
return subscribe((s) => {
if (s.phase !== "cover" || !s.href || phase.current !== "idle") return;
const el = sheet.current;
if (!el) return;
const href = s.href;
phase.current = "cover";
setLabel(s.label ?? chrome.curtain.fallbackLabel);
gsap.killTweensOf(el);
gsap.set(el, { y: 0, yPercent: 100, autoAlpha: 1, pointerEvents: "auto" });
gsap.to(el, {
yPercent: 0,
duration: 0.45,
ease: EASE.inOut,
onComplete: () => {
phase.current = "pushed";
setTransitionPhase("pushed");
/* Sprachwechsel: den Dokumentwechsel macht der Browser, nicht der Router
(Begründung in crossesLangRoot(), src/lib/i18n.ts). Die Hülle bleibt
liegen, bis das neue Dokument da ist – sie gehört zu diesem hier. */
if (crossesLangRoot(pathname, href)) {
window.location.assign(href);
return;
}
router.push(href, { scroll: false });
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => {
if (phase.current === "pushed") window.location.assign(href);
}, 900);
},
});
});
}, [router, pathname, chrome.curtain.fallbackLabel]);
// Route changed → the sheet continues upward and resets.
useEffect(() => {
if (phase.current !== "pushed") return;
const el = sheet.current;
if (!el) return;
window.clearTimeout(timer.current);
phase.current = "reveal";
setTransitionPhase("reveal");
gsap.to(el, {
yPercent: -100,
duration: 0.45,
ease: EASE.inOut,
delay: 0.05,
onComplete: () => {
gsap.set(el, { y: 0, yPercent: 100, autoAlpha: 0, pointerEvents: "none" });
phase.current = "idle";
setLabel(null);
endTransition();
},
});
}, [pathname]);
useEffect(() => () => window.clearTimeout(timer.current), []);
return (
{children ?? }
{label}
);
}
export default Curtain;