Initial commit
This commit is contained in:
57
src/components/navbar/HamburgerButton.tsx
Normal file
57
src/components/navbar/HamburgerButton.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface HamburgerButtonProps {
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
activeBarClassName?: string;
|
||||
inactiveBarClassName?: string;
|
||||
ariaControls?: string;
|
||||
}
|
||||
|
||||
const HamburgerButton = ({
|
||||
isActive,
|
||||
onClick,
|
||||
className = "",
|
||||
activeBarClassName = "bg-background",
|
||||
inactiveBarClassName = "bg-foreground",
|
||||
ariaControls = "navigation-menu",
|
||||
}: HamburgerButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cls(
|
||||
"pointer-events-auto cursor-pointer bg-transparent border-none flex justify-center items-center h-9 w-[var(--height-9)] aspect-square relative",
|
||||
className
|
||||
)}
|
||||
aria-label={isActive ? "Close menu" : "Open menu"}
|
||||
aria-expanded={isActive}
|
||||
aria-controls={ariaControls}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cls(
|
||||
"transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)] w-[40%] h-0.25 absolute",
|
||||
isActive
|
||||
? `${activeBarClassName} translate-y-0 rotate-45`
|
||||
: `${inactiveBarClassName} -translate-y-1 hover:translate-y-1`
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cls(
|
||||
"transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)] w-[40%] h-0.25 absolute",
|
||||
isActive
|
||||
? `${activeBarClassName} translate-y-0 -rotate-45`
|
||||
: `${inactiveBarClassName} translate-y-1 hover:-translate-y-1`
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
HamburgerButton.displayName = "HamburgerButton";
|
||||
|
||||
export default HamburgerButton;
|
||||
66
src/components/navbar/Logo.tsx
Normal file
66
src/components/navbar/Logo.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
// import Image from "next/image";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useButtonClick } from "@/components/button/useButtonClick";
|
||||
|
||||
interface LogoProps {
|
||||
// logoSrc?: string;
|
||||
// logoAlt?: string;
|
||||
brandName?: string;
|
||||
// className?: string;
|
||||
// imageClassName?: string;
|
||||
textClassName?: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const Logo = ({
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
brandName = "Webild",
|
||||
// className = "",
|
||||
// imageClassName = "",
|
||||
textClassName = "",
|
||||
onClick,
|
||||
href,
|
||||
}: LogoProps) => {
|
||||
const handleClick = useButtonClick(href, onClick);
|
||||
|
||||
// if (logoSrc) {
|
||||
// return (
|
||||
// <div className={cls("relative h-[var(--text-xl)] w-auto", className)}>
|
||||
// <Image
|
||||
// src={logoSrc}
|
||||
// alt={logoAlt}
|
||||
// width={100}
|
||||
// height={24}
|
||||
// className={cls("h-full w-auto object-contain", imageClassName)}
|
||||
// unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
const isClickable = onClick || href;
|
||||
|
||||
if (isClickable) {
|
||||
return (
|
||||
<button onClick={handleClick} className="cursor-pointer">
|
||||
<h2 className={cls("text-xl font-medium text-foreground", textClassName)}>
|
||||
{brandName}
|
||||
</h2>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<h2 className={cls("text-xl font-medium text-foreground", textClassName)}>
|
||||
{brandName}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
||||
Logo.displayName = "Logo";
|
||||
|
||||
export default Logo;
|
||||
81
src/components/navbar/NavbarLayoutFloatingInline.tsx
Normal file
81
src/components/navbar/NavbarLayoutFloatingInline.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import Button from "../button/Button";
|
||||
import ButtonTextUnderline from "../button/ButtonTextUnderline";
|
||||
import Logo from "./Logo";
|
||||
import { NavItem } from "@/types/navigation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface NavbarLayoutFloatingInlineProps {
|
||||
navItems: NavItem[];
|
||||
// logoSrc?: string;
|
||||
// logoAlt?: string;
|
||||
brandName?: string;
|
||||
button: ButtonConfig;
|
||||
className?: string;
|
||||
navItemClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoOnClick?: () => void;
|
||||
logoHref?: string;
|
||||
}
|
||||
|
||||
const NavbarLayoutFloatingInline = ({
|
||||
navItems,
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
brandName = "Webild",
|
||||
button,
|
||||
className = "",
|
||||
navItemClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoOnClick,
|
||||
logoHref,
|
||||
}: NavbarLayoutFloatingInlineProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
className="fixed z-[100] top-6 w-full transition-all duration-500 ease-in-out"
|
||||
>
|
||||
<div className={cls(
|
||||
"w-content-width mx-auto",
|
||||
"flex items-center justify-between",
|
||||
"card rounded-theme",
|
||||
"p-3 pl-6 h-fit relative",
|
||||
className
|
||||
)}>
|
||||
<Logo brandName={brandName} onClick={logoOnClick} href={logoHref} />
|
||||
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 hidden md:flex gap-6 items-center">
|
||||
{navItems.map((item, index) => (
|
||||
<ButtonTextUnderline
|
||||
key={index}
|
||||
text={item.name}
|
||||
href={item.id}
|
||||
className={cls("!text-base", navItemClassName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
button,
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
buttonClassName,
|
||||
buttonTextClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarLayoutFloatingInline;
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import ExpandingMenu from "../expandingMenu/ExpandingMenu";
|
||||
import Button from "../../button/Button";
|
||||
import Logo from "../Logo";
|
||||
import { useScrollDetection } from "./useScrollDetection";
|
||||
import { useMenuAnimation } from "./useMenuAnimation";
|
||||
import { useResponsive } from "./useResponsive";
|
||||
import type { NavItem } from "@/types/navigation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
interface NavbarLayoutFloatingOverlayProps {
|
||||
navItems: NavItem[];
|
||||
// logoSrc?: string;
|
||||
// logoAlt?: string;
|
||||
className?: string;
|
||||
brandName?: string;
|
||||
button: ButtonConfig;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
logoOnClick?: () => void;
|
||||
logoHref?: string;
|
||||
}
|
||||
|
||||
const NavbarLayoutFloatingOverlay = ({
|
||||
navItems,
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
className = "",
|
||||
brandName = "Webild",
|
||||
button,
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
logoOnClick,
|
||||
logoHref,
|
||||
}: NavbarLayoutFloatingOverlayProps) => {
|
||||
const theme = useTheme();
|
||||
const isScrolled = useScrollDetection(50);
|
||||
const { menuOpen, buttonZIndex, handleMenuToggle } =
|
||||
useMenuAnimation();
|
||||
const isMobile = useResponsive(768);
|
||||
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
className="fixed z-[100] top-6 w-full transition-all duration-500 ease-in-out"
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"w-content-width mx-auto",
|
||||
"flex items-center justify-between",
|
||||
"card rounded-theme backdrop-blur-xs",
|
||||
"px-6 md:pr-3",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
height: "calc(var(--vw-0_75) + var(--vw-0_75) + var(--height-9))",
|
||||
}}
|
||||
>
|
||||
<Logo brandName={brandName} onClick={logoOnClick} href={logoHref} />
|
||||
<div
|
||||
className="flex items-center transition-all duration-500 ease-in-out"
|
||||
style={{ paddingRight: "calc(var(--height-9) + var(--vw-0_75))" }}
|
||||
>
|
||||
{!isMobile && (
|
||||
<div className="hidden md:flex">
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
button,
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
cls(buttonZIndex, buttonClassName),
|
||||
buttonTextClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ExpandingMenu
|
||||
isOpen={menuOpen}
|
||||
onToggle={handleMenuToggle}
|
||||
navItems={navItems}
|
||||
isScrolled={isScrolled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarLayoutFloatingOverlay;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
export const useMenuAnimation = () => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [buttonZIndex, setButtonZIndex] = useState('z-[100]');
|
||||
const [animationTimeoutId, setAnimationTimeoutId] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMenuToggle = useCallback(() => {
|
||||
const isOpening = !menuOpen;
|
||||
setMenuOpen(prev => !prev);
|
||||
|
||||
if (animationTimeoutId) {
|
||||
clearTimeout(animationTimeoutId);
|
||||
}
|
||||
|
||||
if (isOpening) {
|
||||
setButtonZIndex('z-0');
|
||||
} else {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setButtonZIndex('z-[100]');
|
||||
}, 800);
|
||||
setAnimationTimeoutId(timeoutId);
|
||||
}
|
||||
}, [menuOpen, animationTimeoutId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animationTimeoutId) {
|
||||
clearTimeout(animationTimeoutId);
|
||||
}
|
||||
};
|
||||
}, [animationTimeoutId]);
|
||||
|
||||
return {
|
||||
menuOpen,
|
||||
buttonZIndex,
|
||||
handleMenuToggle,
|
||||
setMenuOpen
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
|
||||
export const useResponsive = (breakpoint: number = 768) => {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = throttle(() => {
|
||||
setIsMobile(window.innerWidth < breakpoint);
|
||||
}, 150);
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [breakpoint]);
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
|
||||
export const useScrollDetection = (threshold: number = 50) => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = throttle(() => {
|
||||
setIsScrolled(window.scrollY > threshold);
|
||||
}, 100);
|
||||
|
||||
handleScroll();
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [threshold]);
|
||||
|
||||
return isScrolled;
|
||||
};
|
||||
132
src/components/navbar/NavbarStyleApple/NavbarStyleApple.tsx
Normal file
132
src/components/navbar/NavbarStyleApple/NavbarStyleApple.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import MobileMenu from "../mobileMenu/MobileMenu";
|
||||
import Button from "@/components/button/Button";
|
||||
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
|
||||
import Logo from "../Logo";
|
||||
import { Plus } from "lucide-react";
|
||||
import { NavbarProps } from "@/types/navigation";
|
||||
import { useScrollState } from "./useScrollState";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
|
||||
const SCROLL_THRESHOLD = 50;
|
||||
|
||||
interface NavbarStyleAppleProps extends NavbarProps {
|
||||
button?: ButtonConfig;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
}
|
||||
|
||||
const NavbarStyleApple = ({
|
||||
navItems,
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
brandName = "Webild",
|
||||
logoOnClick,
|
||||
logoHref,
|
||||
button,
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: NavbarStyleAppleProps) => {
|
||||
const isScrolled = useScrollState(SCROLL_THRESHOLD);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleMenuToggle = useCallback(() => {
|
||||
setMenuOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleMobileNavClick = useCallback(() => {
|
||||
setMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cls(
|
||||
"fixed z-[1000] top-0 left-0 w-full transition-all duration-500 ease-in-out",
|
||||
isScrolled
|
||||
? "bg-background/80 backdrop-blur-sm h-15"
|
||||
: "bg-background/0 backdrop-blur-0 h-20"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between h-full w-content-width mx-auto">
|
||||
<div className="flex items-center transition-all duration-500 ease-in-out">
|
||||
<Logo brandName={brandName} onClick={logoOnClick} href={logoHref} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="hidden md:flex items-center gap-6 transition-all duration-500 ease-in-out"
|
||||
role="navigation"
|
||||
>
|
||||
{navItems.map((item, index) => (
|
||||
<ButtonTextUnderline
|
||||
key={index}
|
||||
text={item.name}
|
||||
href={item.id}
|
||||
className="!text-base"
|
||||
/>
|
||||
))}
|
||||
{button && (
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
button,
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
buttonClassName,
|
||||
buttonTextClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex md:hidden shrink-0 h-8 aspect-square rounded-theme bg-foreground items-center justify-center cursor-pointer"
|
||||
onClick={handleMenuToggle}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<Plus
|
||||
className={cls(
|
||||
"w-1/2 h-1/2 text-background transition-transform duration-300",
|
||||
menuOpen ? "rotate-45" : "rotate-0"
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MobileMenu
|
||||
menuOpen={menuOpen}
|
||||
onMenuToggle={handleMenuToggle}
|
||||
navItems={navItems}
|
||||
onNavClick={handleMobileNavClick}
|
||||
>
|
||||
{button && (
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
{
|
||||
...button,
|
||||
onClick: () => {
|
||||
button.onClick?.();
|
||||
setMenuOpen(false);
|
||||
},
|
||||
},
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
cls("w-full", buttonClassName),
|
||||
buttonTextClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</MobileMenu>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarStyleApple;
|
||||
28
src/components/navbar/NavbarStyleApple/useScrollState.ts
Normal file
28
src/components/navbar/NavbarStyleApple/useScrollState.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export const useScrollState = (threshold: number = 50) => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const rafRef = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
setIsScrolled(window.scrollY > threshold);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
};
|
||||
}, [threshold]);
|
||||
|
||||
return isScrolled;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/* Overlay - active state */
|
||||
[data-navigation-status="active"] .centered-nav__overlay {
|
||||
opacity: 0.15;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Content expand - grid animation */
|
||||
.centered-nav__content {
|
||||
grid-template-rows: 0fr;
|
||||
}
|
||||
|
||||
[data-navigation-status="active"] .centered-nav__content {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
/* Inner container - height hack for grid animation */
|
||||
.centered-nav__inner {
|
||||
height: 10000%;
|
||||
}
|
||||
|
||||
/* Link content slide up animation */
|
||||
.centered-nav__link-content {
|
||||
transition: transform 0.6s cubic-bezier(0.65, 0, 0, 1);
|
||||
transform: translateY(150%);
|
||||
transition-delay: inherit;
|
||||
}
|
||||
|
||||
[data-navigation-status="active"] .centered-nav__link-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Separator width animation */
|
||||
.centered-nav__separator {
|
||||
width: 0;
|
||||
transition: width 0.6s cubic-bezier(0.65, 0, 0, 1);
|
||||
}
|
||||
|
||||
[data-navigation-status="active"] .centered-nav__separator {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, Fragment } from "react";
|
||||
import Logo from "../Logo";
|
||||
import HamburgerButton from "../HamburgerButton";
|
||||
import Button from "@/components/button/Button";
|
||||
import { NavItem } from "@/types/navigation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useButtonClick } from "@/components/button/useButtonClick";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import "./NavbarStyleCentered.css";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
interface NavLinkProps {
|
||||
item: NavItem;
|
||||
index: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const NavLink = ({ item, index, onClose }: NavLinkProps) => {
|
||||
const handleClick = useButtonClick(item.id, onClose);
|
||||
|
||||
return (
|
||||
<li
|
||||
className="group m-0 p-0 list-none overflow-clip"
|
||||
style={{ transitionDelay: `${index * 0.05}s` }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="centered-nav__link relative flex justify-between items-center no-underline w-full text-left bg-transparent border-none cursor-pointer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="centered-nav__link-content flex justify-between items-center gap-3 w-full">
|
||||
<p className="m-0 text-xl md:text-2xl text-foreground truncate group-hover:ml-3 transition-[margin] duration-300">{item.name}</p>
|
||||
<ArrowUpRight className="h-[var(--text-xl)] md:h-[var(--text-2xl)] w-auto text-foreground group-hover:rotate-45 group-hover:mr-3 transition-all duration-300" strokeWidth={1.5} />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavbarStyleCenteredProps {
|
||||
navItems: NavItem[];
|
||||
button: ButtonConfig;
|
||||
// logoSrc?: string;
|
||||
// logoAlt?: string;
|
||||
brandName?: string;
|
||||
className?: string;
|
||||
logoOnClick?: () => void;
|
||||
logoHref?: string;
|
||||
}
|
||||
|
||||
const NavbarStyleCentered = ({
|
||||
navItems,
|
||||
button,
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
brandName = "Webild",
|
||||
className = "",
|
||||
logoOnClick,
|
||||
logoHref,
|
||||
}: NavbarStyleCenteredProps) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const getButtonConfigProps = () => {
|
||||
if (theme.defaultButtonVariant === "hover-bubble") {
|
||||
return { bgClassName: "w-full" };
|
||||
}
|
||||
if (theme.defaultButtonVariant === "icon-arrow") {
|
||||
return { className: "justify-between" };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setIsActive((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsActive(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && isActive) {
|
||||
setIsActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
data-navigation-status={isActive ? "active" : "not-active"}
|
||||
className="fixed inset-0 z-[1000] pointer-events-none"
|
||||
>
|
||||
<div
|
||||
className="centered-nav__overlay absolute inset-0 bg-foreground pointer-events-auto opacity-0 invisible transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<div className={cls("pointer-events-auto absolute top-6 left-1/2 -translate-x-1/2 w-content-width md:top-8 md:w-35 flex flex-col justify-start items-stretch rounded-theme-capped", className)} >
|
||||
<div className="absolute! inset-0 card backdrop-blur-xs rounded-theme-capped" />
|
||||
<div className="relative z-10 flex justify-between items-center py-3 px-6">
|
||||
<Logo
|
||||
brandName={brandName}
|
||||
onClick={logoOnClick}
|
||||
href={logoHref}
|
||||
/>
|
||||
<HamburgerButton
|
||||
isActive={isActive}
|
||||
onClick={handleToggle}
|
||||
activeBarClassName="bg-foreground"
|
||||
inactiveBarClassName="bg-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="centered-nav__content relative overflow-hidden rounded-b-theme-capped grid transition-[grid-template-rows] duration-600 ease-[cubic-bezier(0.625,0.05,0,1)]">
|
||||
<div className="centered-nav__inner flex flex-col justify-start items-center w-full relative overflow-hidden gap-6">
|
||||
<div className="w-full px-6" >
|
||||
<ul className="relative w-full card p-6 rounded-theme-capped flex flex-col gap-3 justify-start items-stretch m-0 list-none">
|
||||
{navItems.map((item, index) => (
|
||||
<Fragment key={item.id}>
|
||||
<NavLink item={item} index={index} onClose={handleClose} />
|
||||
{index < navItems.length - 1 && <div className="centered-nav__separator h-px bg-accent/50" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full px-6 pb-6">
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
{
|
||||
...button,
|
||||
onClick: () => {
|
||||
button.onClick?.();
|
||||
handleClose();
|
||||
},
|
||||
props: { ...button.props, ...getButtonConfigProps() }
|
||||
},
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
"w-full"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
NavbarStyleCentered.displayName = "NavbarStyleCentered";
|
||||
|
||||
export default NavbarStyleCentered;
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Tile clip-path animation */
|
||||
.navbar-fullscreen__tile {
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%);
|
||||
transition: clip-path 1s cubic-bezier(.9, 0, .1, 1);
|
||||
}
|
||||
|
||||
[data-navigation-status="active"] .navbar-fullscreen__tile {
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
|
||||
}
|
||||
|
||||
/* Link initial state and animation */
|
||||
.navbar-fullscreen__link {
|
||||
transform: translateY(100%) rotate(5deg);
|
||||
transition: transform 0.75s cubic-bezier(.7, 0, .3, 1);
|
||||
}
|
||||
|
||||
/* Link staggered delays (closing) */
|
||||
.navbar-fullscreen__li:nth-child(1) .navbar-fullscreen__link { transition-delay: 0.2s; }
|
||||
.navbar-fullscreen__li:nth-child(2) .navbar-fullscreen__link { transition-delay: 0.15s; }
|
||||
.navbar-fullscreen__li:nth-child(3) .navbar-fullscreen__link { transition-delay: 0.1s; }
|
||||
.navbar-fullscreen__li:nth-child(4) .navbar-fullscreen__link { transition-delay: 0.05s; }
|
||||
.navbar-fullscreen__li:nth-child(5) .navbar-fullscreen__link { transition-delay: 0s; }
|
||||
|
||||
/* Link animation (Navigation Open) */
|
||||
[data-navigation-status="active"] .navbar-fullscreen__link {
|
||||
transform: translateY(0%) rotate(0.001deg);
|
||||
}
|
||||
|
||||
/* Link staggered delays (opening) */
|
||||
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(1) .navbar-fullscreen__link { transition-delay: 0.3s; }
|
||||
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(2) .navbar-fullscreen__link { transition-delay: 0.35s; }
|
||||
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(3) .navbar-fullscreen__link { transition-delay: 0.4s; }
|
||||
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(4) .navbar-fullscreen__link { transition-delay: 0.45s; }
|
||||
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(5) .navbar-fullscreen__link { transition-delay: 0.5s; }
|
||||
|
||||
/* Link text duplicate effect */
|
||||
.navbar-fullscreen__link-text {
|
||||
text-shadow: 0 1.1em 0;
|
||||
transition: transform 0.5s cubic-bezier(.7, 0, .3, 1);
|
||||
transform: translateY(0%) rotate(0.001deg);
|
||||
}
|
||||
|
||||
.navbar-fullscreen__link:hover .navbar-fullscreen__link-text {
|
||||
transform: translateY(-100%) rotate(0.001deg);
|
||||
}
|
||||
|
||||
/* Hover dim effect on siblings */
|
||||
.navbar-fullscreen__li {
|
||||
transition: opacity 0.5s cubic-bezier(.7, 0, .3, 1);
|
||||
}
|
||||
|
||||
.navbar-fullscreen__ul:has(.navbar-fullscreen__li:hover) .navbar-fullscreen__li {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.navbar-fullscreen__ul:has(.navbar-fullscreen__li:hover) .navbar-fullscreen__li:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Logo from "../Logo";
|
||||
import HamburgerButton from "../HamburgerButton";
|
||||
import Button from "@/components/button/Button";
|
||||
import { NavItem } from "@/types/navigation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { useButtonClick } from "@/components/button/useButtonClick";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import "./NavbarStyleFullscreen.css";
|
||||
|
||||
interface NavLinkProps {
|
||||
item: NavItem;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const NavLink = ({ item, onClose }: NavLinkProps) => {
|
||||
const handleClick = useButtonClick(item.id, onClose);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="navbar-fullscreen__link text-background font-normal leading-[1.15] no-underline text-9xl bg-transparent border-none cursor-pointer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="navbar-fullscreen__link-text block relative">{item.name}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavbarStyleFullscreenProps {
|
||||
navItems: NavItem[];
|
||||
// logoSrc?: string;
|
||||
// logoAlt?: string;
|
||||
brandName?: string;
|
||||
bottomLeftText?: string;
|
||||
bottomRightText?: string;
|
||||
topBarClassName?: string;
|
||||
logoOnClick?: () => void;
|
||||
logoHref?: string;
|
||||
button?: ButtonConfig;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
}
|
||||
|
||||
const NavbarStyleFullscreen = ({
|
||||
navItems,
|
||||
// logoSrc,
|
||||
// logoAlt = "",
|
||||
brandName = "Webild",
|
||||
bottomLeftText = "Global Community",
|
||||
bottomRightText = "hello@example.com",
|
||||
topBarClassName = "",
|
||||
logoOnClick,
|
||||
logoHref,
|
||||
button,
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: NavbarStyleFullscreenProps) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setIsActive((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsActive(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && isActive) {
|
||||
setIsActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
data-navigation-status={isActive ? "active" : "not-active"}
|
||||
className="fixed inset-0 z-[100] pointer-events-none"
|
||||
>
|
||||
<div className={cls(
|
||||
"absolute z-1 w-content-width left-1/2 -translate-x-1/2 top-6 flex justify-between items-center pointer-events-auto",
|
||||
topBarClassName
|
||||
)}>
|
||||
<Logo
|
||||
brandName={brandName}
|
||||
textClassName={`transition-colors duration-700 ease-[cubic-bezier(0.5,0.5,0,1)] ${isActive ? "text-background" : "text-foreground"}`}
|
||||
onClick={logoOnClick}
|
||||
href={logoHref}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
{button && (
|
||||
<Button
|
||||
{...getButtonProps(
|
||||
button,
|
||||
0,
|
||||
theme.defaultButtonVariant,
|
||||
buttonClassName,
|
||||
buttonTextClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<HamburgerButton isActive={isActive} onClick={handleToggle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="navigation-menu"
|
||||
className="navbar-fullscreen__tile pointer-events-auto bg-foreground backdrop-blur absolute inset-0 flex flex-col justify-center items-center"
|
||||
>
|
||||
<ul className="navbar-fullscreen__ul flex flex-col items-center m-0 p-0 list-none">
|
||||
{navItems.map((item) => (
|
||||
<li key={item.id} className="navbar-fullscreen__li flex justify-center items-center m-0 p-0 relative overflow-hidden">
|
||||
<NavLink item={item} onClose={handleClose} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="absolute bottom-0 w-content-width left-1/2 -translate-x-1/2 flex justify-between items-center py-10">
|
||||
<p className="text-background/50 mb-0 text-base relative">{bottomLeftText}</p>
|
||||
<p className="text-background/50 mb-0 text-base relative">{bottomRightText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
NavbarStyleFullscreen.displayName = "NavbarStyleFullscreen";
|
||||
|
||||
export default NavbarStyleFullscreen;
|
||||
149
src/components/navbar/expandingMenu/ExpandingMenu.tsx
Normal file
149
src/components/navbar/expandingMenu/ExpandingMenu.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useResponsiveMenuWidth } from './useResponsiveMenuWidth';
|
||||
import { useButtonClick } from '@/components/button/useButtonClick';
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ExpandingMenuProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
navItems: NavItem[];
|
||||
isScrolled?: boolean;
|
||||
}
|
||||
|
||||
const ExpandingMenu = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
navItems,
|
||||
isScrolled = false
|
||||
}: ExpandingMenuProps) => {
|
||||
const { isMounted, menuWidth } = useResponsiveMenuWidth();
|
||||
|
||||
const handleNavClick = useCallback(() => {
|
||||
onToggle();
|
||||
}, [onToggle]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
rounded-theme-capped absolute top-[calc(var(--vw-0_75)-1px)] right-3
|
||||
transition-[top] duration-500 ease-in-out
|
||||
${isScrolled ? '' : ''}
|
||||
${isOpen ? 'pointer-events-auto' : 'pointer-events-none'}
|
||||
`}>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
primary-button
|
||||
backdrop-blur-xs
|
||||
transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
|
||||
bg-foreground rounded-theme-capped absolute top-0 right-0
|
||||
${isOpen
|
||||
? 'w-full h-full'
|
||||
: 'h-9 w-[var(--height-9)]'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
<div className={`
|
||||
relative p-6 flex flex-col gap-6
|
||||
transition-all duration-500 ease-[cubic-bezier(0.5,0.5,0,1)]
|
||||
origin-[100%_0]
|
||||
${isOpen
|
||||
? 'scale-100 opacity-100 visible pointer-events-auto'
|
||||
: 'scale-[0.15] opacity-0 invisible pointer-events-none'
|
||||
}
|
||||
`}
|
||||
style={{
|
||||
transition: 'all 0.5s cubic-bezier(0.5, 0.5, 0, 1), transform 0.7s cubic-bezier(0.5, 0.5, 0, 1)',
|
||||
width: isMounted ? menuWidth : 'var(--width-20)'
|
||||
}}
|
||||
>
|
||||
<p className="text-xl text-background" aria-hidden="true">Menu</p>
|
||||
<ul
|
||||
role="menu"
|
||||
className="relative list-none flex flex-col gap-3 m-0 p-0"
|
||||
>
|
||||
{navItems.map((item) => {
|
||||
const MenuButton = () => {
|
||||
const handleClick = useButtonClick(item.id, handleNavClick);
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={`Navigate to ${item.name}`}
|
||||
className={`
|
||||
text-background flex justify-between items-center
|
||||
no-underline bg-none border-none cursor-pointer w-full
|
||||
font-inherit group
|
||||
`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="text-base">
|
||||
{item.name}
|
||||
</span>
|
||||
<div className="bg-current rounded-theme-capped shrink-0 h-2 aspect-square" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
key={item.id}
|
||||
role="menuitem"
|
||||
className="m-0 p-0 list-none"
|
||||
>
|
||||
<MenuButton />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label={isOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="navigation-menu"
|
||||
className={`
|
||||
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
|
||||
pointer-events-auto cursor-pointer rounded-theme-capped
|
||||
flex justify-center items-center
|
||||
h-9 w-[var(--height-9)] aspect-square absolute top-0 right-0
|
||||
bg-transparent border-none
|
||||
${isOpen
|
||||
? '-translate-x-3 translate-y-3'
|
||||
: 'translate-x-0 translate-y-0'
|
||||
}
|
||||
`}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
|
||||
bg-background w-[40%] h-0.25 absolute
|
||||
${isOpen
|
||||
? 'translate-y-0 rotate-45'
|
||||
: '-translate-y-1 hover:translate-y-1'
|
||||
}
|
||||
`} />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
|
||||
bg-background w-[40%] h-0.25 absolute
|
||||
${isOpen
|
||||
? 'translate-y-0 -rotate-45'
|
||||
: 'translate-y-1 hover:-translate-y-1'
|
||||
}
|
||||
`} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpandingMenu;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useResponsiveMenuWidth = () => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [menuWidth, setMenuWidth] = useState('var(--width-20)');
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
const handleResize = () => {
|
||||
setMenuWidth(
|
||||
window.innerWidth >= 768
|
||||
? 'var(--width-20)'
|
||||
: 'calc(var(--width-80) - var(--vw-0_75) * 2)'
|
||||
);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return { isMounted, menuWidth };
|
||||
};
|
||||
79
src/components/navbar/mobileMenu/MobileMenu.tsx
Normal file
79
src/components/navbar/mobileMenu/MobileMenu.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import { ArrowRight, Plus } from 'lucide-react';
|
||||
import { NavItem } from '@/types/navigation';
|
||||
import { useMenuAnimation } from './useMenuAnimation';
|
||||
import { useButtonClick } from '@/components/button/useButtonClick';
|
||||
|
||||
interface MobileMenuProps {
|
||||
menuOpen: boolean;
|
||||
onMenuToggle: () => void;
|
||||
navItems: NavItem[];
|
||||
onNavClick: (id: string) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MobileMenu = ({
|
||||
menuOpen,
|
||||
onMenuToggle,
|
||||
navItems,
|
||||
onNavClick,
|
||||
children
|
||||
}: MobileMenuProps) => {
|
||||
const menuRef = useMenuAnimation(menuOpen);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="mobile-menu"
|
||||
className="md:hidden z-10 fixed top-3 left-1/2 -translate-x-1/2 h-fit rounded-theme-capped card p-6 flex flex-col gap-6 opacity-0"
|
||||
style={{ width: 'calc(100vw - var(--vw-0_75) * 2)' }}
|
||||
ref={menuRef}
|
||||
role="navigation"
|
||||
aria-label="Mobile navigation menu"
|
||||
>
|
||||
<div className="w-full flex justify-between items-center">
|
||||
<p className="text-xl text-foreground">Menu</p>
|
||||
<button
|
||||
className="shrink-0 h-8 aspect-square rounded-theme bg-foreground flex items-center justify-center cursor-pointer"
|
||||
onClick={onMenuToggle}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<Plus className="w-1/2 h-1/2 text-background rotate-45" strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{navItems.map((item, index) => {
|
||||
const NavButton = () => {
|
||||
const handleClick = useButtonClick(item.id, () => onNavClick(item.id));
|
||||
|
||||
return (
|
||||
<button
|
||||
className="w-full h-fit flex justify-between items-center cursor-pointer"
|
||||
onClick={handleClick}
|
||||
aria-label={`Navigate to ${item.name}`}
|
||||
>
|
||||
<p className="text-base font-medium">{item.name}</p>
|
||||
<ArrowRight strokeWidth={1.5} className="h-[var(--text-base)] w-auto text-foreground" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<NavButton />
|
||||
{index < navItems.length - 1 && (
|
||||
<div className="w-full h-px bg-gradient-to-r from-transparent via-foreground/20 to-transparent" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{children && (
|
||||
<div className="flex gap-3 items-center">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileMenu;
|
||||
40
src/components/navbar/mobileMenu/useMenuAnimation.ts
Normal file
40
src/components/navbar/mobileMenu/useMenuAnimation.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
export const useMenuAnimation = (menuOpen: boolean) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuRef.current) {
|
||||
if (menuOpen) {
|
||||
gsap.to(menuRef.current, {
|
||||
y: "0%",
|
||||
opacity: 1,
|
||||
pointerEvents: "auto",
|
||||
duration: 0.8,
|
||||
ease: "power3.out"
|
||||
});
|
||||
} else {
|
||||
gsap.to(menuRef.current, {
|
||||
y: "-135%",
|
||||
opacity: 1,
|
||||
pointerEvents: "none",
|
||||
duration: 0.8,
|
||||
ease: "power3.inOut"
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [menuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuRef.current) {
|
||||
gsap.set(menuRef.current, {
|
||||
y: "-135%",
|
||||
opacity: 1,
|
||||
pointerEvents: "none"
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return menuRef;
|
||||
};
|
||||
Reference in New Issue
Block a user