Add responsive nav link stretch

This commit is contained in:
2026-05-16 15:08:06 +01:00
parent a5d9eed843
commit ad070a21a5
2 changed files with 48 additions and 7 deletions

View File

@@ -9,6 +9,7 @@ All notable changes to the New Optic website will be documented in this file.
- Removed the desktop liquid glass WebGL layer from navbar and CTA controls so text remains readable and glass surfaces do not show dark rendering artifacts while scrolling.
- Replaced the mobile hamburger dropdown with a fast, solid menu animation without bounce or stretch effects.
- Kept the language switcher visible in the mobile top navigation bar.
- Added cursor-reactive stretch motion to desktop nav links and matched the glossy dark WhatsApp CTA style on mobile.
## [1.0.0] - 2026-05-16

View File

@@ -1,14 +1,17 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { Menu, X } from "lucide-react";
import Image from "next/image";
import { type MouseEvent, useState } from "react";
import { type MouseEvent, type PointerEvent, useState } from "react";
import { business, type Locale } from "@/config/business";
import type { Messages } from "@/messages";
import LanguageSwitcher from "./LanguageSwitcher";
import PhysicsButton from "./PhysicsButton";
const navCtaClassName =
"rounded-full border border-white/45 bg-[linear-gradient(180deg,#565b63_0%,#20242a_48%,#111317_100%)] font-semibold text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.38),inset_0_-10px_18px_rgba(0,0,0,0.18),0_14px_32px_rgba(17,19,23,0.18)] transition-colors hover:bg-[linear-gradient(180deg,#476a95_0%,#264b73_54%,#173655_100%)]";
export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { locale: Locale; onLocaleChange: (locale: Locale) => void; t: Messages; whatsappUrl: string }) {
const [open, setOpen] = useState(false);
@@ -34,22 +37,20 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
<div className="relative z-[3] hidden items-center gap-7 lg:flex">
{t.nav.links.map((link) => (
<a key={link.href} href={link.href} onClick={(event) => scrollToSection(event, link.href)} className="text-sm font-medium text-ink/58 transition hover:text-ink">
{link.label}
</a>
<StretchNavLink key={link.href} href={link.href} label={link.label} onClick={(event) => scrollToSection(event, link.href)} />
))}
</div>
<div className="relative z-[3] hidden items-center gap-3 md:flex">
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} />
<PhysicsButton href={whatsappUrl} external className="rounded-full bg-ink px-5 py-2.5 text-sm font-semibold text-white shadow-soft transition-colors hover:bg-optical">
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} px-5 py-2.5`}>
{t.nav.cta}
</PhysicsButton>
</div>
<div className="relative z-[3] flex items-center gap-2 md:hidden">
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} className="bg-white/80 backdrop-blur-none" buttonClassName="px-2 sm:px-3" />
<PhysicsButton href={whatsappUrl} external className="rounded-full bg-ink px-3 py-2.5 text-xs font-semibold text-white shadow-soft transition-colors hover:bg-optical max-[374px]:hidden sm:px-5 sm:text-sm">
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} px-3 py-2.5 text-xs max-[374px]:hidden sm:px-5 sm:text-sm`}>
{t.nav.cta}
</PhysicsButton>
<motion.button
@@ -97,3 +98,42 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc
</header>
);
}
function StretchNavLink({ href, label, onClick }: { href: string; label: string; onClick: (event: MouseEvent<HTMLAnchorElement>) => void }) {
const pullX = useMotionValue(0);
const pullY = useMotionValue(0);
const x = useSpring(pullX, { stiffness: 260, damping: 16, mass: 0.3 });
const y = useSpring(pullY, { stiffness: 260, damping: 16, mass: 0.3 });
const scaleX = useTransform(x, [-18, 0, 18], [1.12, 1, 1.12]);
const scaleY = useTransform(y, [-12, 0, 12], [0.94, 1, 0.94]);
function handlePointerMove(event: PointerEvent<HTMLAnchorElement>) {
if (event.pointerType === "touch") return;
const bounds = event.currentTarget.getBoundingClientRect();
const localX = event.clientX - bounds.left - bounds.width / 2;
const localY = event.clientY - bounds.top - bounds.height / 2;
pullX.set(Math.max(-18, Math.min(18, localX * 0.42)));
pullY.set(Math.max(-12, Math.min(12, localY * 0.38)));
}
function release() {
pullX.set(0);
pullY.set(0);
}
return (
<motion.a
href={href}
onClick={onClick}
onPointerMove={handlePointerMove}
onPointerLeave={release}
onPointerCancel={release}
style={{ x, y, scaleX, scaleY }}
className="inline-flex min-h-9 items-center rounded-full px-1 text-sm font-medium text-ink/58 will-change-transform transition-colors hover:text-ink"
>
{label}
</motion.a>
);
}