Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React, {
- createContext,
- useContext,
- useState,
- useCallback,
- useEffect,
- useRef,
- useMemo,
- } from "react";
- import { motion, AnimatePresence, type Variants } from "motion/react";
- import { easeOutExpo } from "easing-utils";
- // ----------------------
- // 🔁 Preset Transitions (Optional)
- // ----------------------
- export const PresetTransitions = {
- slide: {
- initial: { x: "100%" },
- animate: { x: 0 },
- exit: { x: "100%" },
- },
- slideLeft: {
- initial: { x: "-100%" },
- animate: { x: 0 },
- exit: { x: "-100%" },
- },
- slideUp: {
- initial: { y: "100%" },
- animate: { y: 0 },
- exit: { y: "100%" },
- },
- slideDown: {
- initial: { y: "-100%" },
- animate: { y: 0 },
- exit: { y: "-100%" },
- },
- fade: {
- initial: { opacity: 0 },
- animate: { opacity: 1 },
- exit: { opacity: 0 },
- },
- scale: {
- initial: { scale: 0.8, opacity: 0 },
- animate: { scale: 1, opacity: 1 },
- exit: { scale: 0.8, opacity: 0 },
- },
- scaleUp: {
- initial: { scale: 0.5, opacity: 0 },
- animate: { scale: 1, opacity: 1 },
- exit: { scale: 0.5, opacity: 0 },
- },
- scaleDown: {
- initial: { scale: 1.2, opacity: 0 },
- animate: { scale: 1, opacity: 1 },
- exit: { scale: 1.2, opacity: 0 },
- },
- flip: {
- initial: { rotateY: 90, opacity: 0 },
- animate: { rotateY: 0, opacity: 1 },
- exit: { rotateY: 90, opacity: 0 },
- },
- rotate: {
- initial: { rotate: -90, opacity: 0 },
- animate: { rotate: 0, opacity: 1 },
- exit: { rotate: 90, opacity: 0 },
- },
- } satisfies Record<string, Variants>;
- // ----------------------
- // 🔠 Type Declarations
- // ----------------------
- interface PageStackItem {
- id: number;
- component: React.ReactElement;
- transition?: Variants;
- }
- interface NavigationContextType {
- push: (component: React.ReactElement, transition?: Variants) => void;
- pop: () => void;
- replace: (component: React.ReactElement, transition?: Variants) => void;
- popToRoot: () => void;
- canGoBack: boolean;
- stackLength: number;
- _registerPagePopState: (
- id: number,
- setPopped: (isPopped: boolean) => void
- ) => () => void;
- }
- // ---------------------------
- // 🌐 Navigation Context Setup
- // ---------------------------
- const NavigationContext = createContext<NavigationContextType | null>(null);
- // ------------------------------
- // 🧭 Stack Navigator Component
- // ------------------------------
- export const StackNavigator = ({
- children,
- initialRoute,
- }: {
- children?: React.ReactElement;
- initialRoute: React.ReactElement;
- }) => {
- const initialPage: PageStackItem = useMemo(
- () => ({
- id: Date.now(),
- component: initialRoute || children!,
- }),
- [initialRoute, children]
- );
- const stackRef = useRef<PageStackItem[]>([initialPage]);
- const [, forceUpdate] = useState(0);
- const pagePopStateSetters = useRef<Map<number, (isPopped: boolean) => void>>(
- new Map()
- );
- const setStackAndRef = useCallback(
- (updater: (prev: PageStackItem[]) => PageStackItem[]) => {
- stackRef.current = updater(stackRef.current);
- forceUpdate((n) => n + 1);
- },
- []
- );
- const push = useCallback(
- (component: React.ReactElement, transition?: Variants) => {
- const newPage: PageStackItem = {
- id: Date.now(),
- component,
- transition,
- };
- if (typeof window !== "undefined") {
- window.history.pushState({ page: "pushed" }, "");
- }
- setStackAndRef((prev) => [...prev, newPage]);
- },
- [setStackAndRef]
- );
- const pop = useCallback(() => {
- if (stackRef.current.length > 1 && typeof window !== "undefined") {
- window.history.back();
- }
- }, []);
- const replace = useCallback(
- (component: React.ReactElement, transition?: Variants) => {
- const newPage: PageStackItem = {
- id: Date.now(),
- component,
- transition,
- };
- setStackAndRef((prev) => {
- const oldPage = prev.at(-1);
- oldPage && pagePopStateSetters.current.get(oldPage.id)?.(true);
- return [...prev.slice(0, -1), newPage];
- });
- },
- [setStackAndRef]
- );
- const popToRoot = useCallback(() => {
- setStackAndRef((prev) => {
- const popsNeeded = prev.length - 1;
- if (typeof window !== "undefined" && popsNeeded > 0) {
- for (let i = 1; i < prev.length; i++) {
- pagePopStateSetters.current.get(prev[i].id)?.(true);
- }
- window.history.go(-popsNeeded);
- }
- return prev;
- });
- }, [setStackAndRef]);
- const _registerPagePopState = useCallback(
- (id: number, setPopped: (isPopped: boolean) => void) => {
- pagePopStateSetters.current.set(id, setPopped);
- return () => pagePopStateSetters.current.delete(id);
- },
- []
- );
- useEffect(() => {
- if (typeof window !== "undefined") {
- window.history.replaceState({ page: "initial" }, "");
- }
- const onPopState = () => {
- setStackAndRef((prev) => {
- if (prev.length > 1) {
- const poppedPage = prev.at(-1);
- poppedPage && pagePopStateSetters.current.get(poppedPage.id)?.(true);
- return prev.slice(0, -1);
- }
- return prev;
- });
- };
- window.addEventListener("popstate", onPopState);
- return () => window.removeEventListener("popstate", onPopState);
- }, [setStackAndRef]);
- const stack = stackRef.current;
- const contextValue = useMemo<NavigationContextType>(
- () => ({
- push,
- pop,
- replace,
- popToRoot,
- canGoBack: stack.length > 1,
- stackLength: stack.length,
- _registerPagePopState,
- }),
- [push, pop, replace, popToRoot, stack.length, _registerPagePopState]
- );
- return (
- <NavigationContext.Provider value={contextValue}>
- <div className="relative w-full h-full overflow-clip">
- <AnimatePresence>
- {stack.map((page, index) => (
- <MemoizedPageContainer
- key={page.id}
- page={page}
- isTop={index === stack.length - 1}
- />
- ))}
- </AnimatePresence>
- </div>
- </NavigationContext.Provider>
- );
- };
- // ------------------------
- // 📦 Page Container
- // ------------------------
- interface PageContainerProps {
- page: PageStackItem;
- isTop: boolean;
- }
- const PageContainer = ({ page, isTop }: PageContainerProps) => {
- const { _registerPagePopState } = useNavigator();
- const [isPopped, setIsPopped] = useState(false);
- useEffect(() => {
- const unregister = _registerPagePopState(page.id, setIsPopped);
- return unregister;
- }, [page.id, _registerPagePopState]);
- const fallbackVariant: Variants = {
- initial: { opacity: 0 },
- animate: { opacity: 1 },
- exit: { opacity: 0 },
- };
- const variants = page.transition ?? fallbackVariant;
- const dimVariants: Variants = useMemo(
- () => ({
- initial: { backgroundColor: "rgba(0,0,0,0)" },
- animate: { backgroundColor: "rgba(0,0,0,0.5)" },
- exit: { backgroundColor: "rgba(0,0,0,0)" },
- }),
- []
- );
- return (
- <div
- className="absolute top-0 left-0 w-full h-full pointer-events-none"
- style={{ zIndex: isTop ? 10 : 1 }}
- >
- <motion.div
- className={`absolute top-0 left-0 w-full h-full`}
- variants={dimVariants}
- initial="initial"
- animate="animate"
- exit="exit"
- transition={{ duration: 0.6, ease: easeOutExpo }}
- style={{ zIndex: 1 }}
- />
- <motion.div
- className={`h-full w-full absolute top-0 left-0 ${
- !isPopped ? "pointer-events-auto" : "pointer-events-none"
- }`}
- initial="initial"
- animate="animate"
- exit="exit"
- variants={variants}
- transition={{ duration: 0.6, ease: easeOutExpo }}
- style={{ zIndex: 2 }}
- >
- {React.isValidElement(page.component)
- ? React.cloneElement(page.component, { isPopped } as any)
- : React.createElement(page.component, { isPopped } as any)}
- </motion.div>
- </div>
- );
- };
- const MemoizedPageContainer = React.memo(PageContainer);
- // ----------------------------
- // 🪝 useNavigator & usePage
- // ----------------------------
- export const useNavigator = (): NavigationContextType => {
- const context = useContext(NavigationContext);
- if (!context) {
- throw new Error("useNavigator must be used within a StackNavigator");
- }
- return context;
- };
- export const usePage = () => {
- const [isPopped, setIsPopped] = useState(false);
- return { isPopped, _setIsPopped: setIsPopped };
- };
Advertisement
Add Comment
Please, Sign In to add comment