diff --git a/CHANGELOG.md b/CHANGELOG.md index 010705d..82289d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the New Optic website will be documented in this file. ### Changed - 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. +- Made the mobile hamburger menu open with springy jelly motion and added a draggable corner grip that stretches the menu before snapping back. ## [1.0.0] - 2026-05-16 diff --git a/components/Navbar.tsx b/components/Navbar.tsx index d89f66c..14ac74f 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -1,7 +1,7 @@ "use client"; -import { AnimatePresence, motion } from "framer-motion"; -import { Menu, X } from "lucide-react"; +import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "framer-motion"; +import { Grip, Menu, X } from "lucide-react"; import Image from "next/image"; import { type MouseEvent, useState } from "react"; import { business, type Locale } from "@/config/business"; @@ -11,6 +11,22 @@ import PhysicsButton from "./PhysicsButton"; export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { locale: Locale; onLocaleChange: (locale: Locale) => void; t: Messages; whatsappUrl: string }) { const [open, setOpen] = useState(false); + const stretchX = useMotionValue(0); + const stretchY = useMotionValue(0); + const springX = useSpring(stretchX, { stiffness: 260, damping: 13, mass: 0.34 }); + const springY = useSpring(stretchY, { stiffness: 260, damping: 13, mass: 0.34 }); + const menuScaleX = useTransform(springX, [-70, 0, 96], [0.9, 1, 1.22]); + const menuScaleY = useTransform(springY, [-54, 0, 104], [0.88, 1, 1.18]); + const menuSkewX = useTransform(springX, [-70, 0, 96], [-5.5, 0, 7]); + const menuRadius = useTransform(springY, [-54, 0, 104], ["2.65rem", "2rem", "2.9rem"]); + const sheenX = useTransform(springX, [-70, 0, 96], ["-18%", "0%", "20%"]); + const cornerX = useSpring(stretchX, { stiffness: 520, damping: 20, mass: 0.24 }); + const cornerY = useSpring(stretchY, { stiffness: 520, damping: 20, mass: 0.24 }); + + function releaseMenuStretch() { + stretchX.set(0); + stretchY.set(0); + } function scrollToSection(event: MouseEvent, href: string) { if (!href.startsWith("#")) return; @@ -51,31 +67,81 @@ export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { loc {t.nav.cta} - + {open ? ( -
- {t.nav.links.map((link) => ( - scrollToSection(event, link.href)} className="rounded-2xl px-4 py-3 text-sm font-semibold text-ink/72 hover:bg-white"> - {link.label} - - ))} -
-
- -
+ + + +
+ {t.nav.links.map((link) => ( + scrollToSection(event, link.href)} + initial={{ opacity: 0, x: locale === "ar" ? -28 : 28, scaleX: 0.86 }} + animate={{ opacity: 1, x: 0, scaleX: 1 }} + transition={{ type: "spring", stiffness: 680, damping: 22, mass: 0.26 }} + whileTap={{ scaleX: 1.12, scaleY: 0.86 }} + className="rounded-2xl px-4 py-3 text-sm font-semibold text-ink/72 hover:bg-white" + > + {link.label} + + ))} +
+
+ +
+ { + stretchX.set(Math.max(-70, Math.min(96, info.offset.x))); + stretchY.set(Math.max(-54, Math.min(104, info.offset.y))); + }} + onDragEnd={releaseMenuStretch} + onPointerUp={releaseMenuStretch} + style={{ x: cornerX, y: cornerY }} + className="absolute bottom-3 right-3 z-[3] grid size-9 touch-none cursor-grab place-items-center rounded-full border border-ink/10 bg-white/80 text-ink/55 shadow-sm outline-none transition focus-visible:ring-2 focus-visible:ring-optical/35 active:cursor-grabbing rtl:left-3 rtl:right-auto" + > + + +
) : null}