Compare commits

...
2 Commits
Author SHA1 Message Date
moh 4df0d9ae86 IMPROVED - Add page exit animation via frozen-router AnimatePresence
CI / quality (push) Canceled after 0s
Wrap the template in AnimatePresence (mode="wait") with a FrozenRouter that
pins the layout-router context, so the outgoing page animates out (blur +
rise) before the incoming page animates in.
2026-09-20 19:50:46 +02:00
moh 5c10bce740 IMPROVED - Replace per-title hero entrance with a unified page transition
- Add (site)/template.tsx: one framer-motion enter animation (fade + rise +
  blur) that runs on every navigation, so moving between dock pages has a
  clean transition.
- Disable the staggered .hero-rise entrance now that the template handles it.
2026-09-20 19:46:58 +02:00
2 changed files with 52 additions and 3 deletions
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { usePathname } from "next/navigation";
import { useContext, useState, type ReactNode } from "react";
/**
* App Router swaps in the new page's content as soon as you navigate, so the
* outgoing page would normally vanish before it can animate out. `FrozenRouter`
* pins the layout-router context to the value it had when this instance
* mounted, keeping the *old* subtree rendered while its exit animation plays.
*/
function FrozenRouter({ children }: { children: ReactNode }) {
const context = useContext(LayoutRouterContext);
// Freeze the context captured at mount so the outgoing subtree keeps
// rendering (with its old route data) while its exit animation plays.
const [frozen] = useState(context);
if (!frozen) {
return <>{children}</>;
}
return <LayoutRouterContext.Provider value={frozen}>{children}</LayoutRouterContext.Provider>;
}
/**
* One unified page transition on every navigation: the old page animates out
* (blur + rise up) before the new page animates in (blur + rise from below).
*/
export default function SiteTemplate({ children }: { children: ReactNode }) {
const reducedMotion = useReducedMotion();
const pathname = usePathname();
return (
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={pathname}
initial={reducedMotion ? false : { opacity: 0, y: 18, filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -18, filter: "blur(10px)" }}
transition={reducedMotion ? { duration: 0 } : { duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
>
<FrozenRouter>{children}</FrozenRouter>
</motion.div>
</AnimatePresence>
);
}
+4 -3
View File
@@ -422,10 +422,11 @@ html[lang="ar"] .eyebrow {
}
}
/* ---- Hero entrance (pure CSS so it never flashes blank before hydration) ---- */
/* ---- Hero entrance ----
Per-element rise is disabled: the site `template.tsx` now runs one unified
page-transition animation on every navigation instead. ---- */
.hero-rise {
opacity: 0;
animation: hero-rise-in 0.62s cubic-bezier(0.22, 1, 0.36, 1) both;
opacity: 1;
}
.hero-content > *:nth-child(1) {