"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 ( ); } export default Curtain;