# Fullscreen Overlay Menu with Skewed Clip-Path Reveal & Hover Image Preview

## Goal

Build a fullscreen overlay navigation menu (ExoApe-style). Clicking a "Menu" toggle fires a set of synchronized GSAP tweens: the page content rotates/scales/translates away to the bottom-right, a dark overlay opens via an animated `clip-path` polygon with a skewed bottom edge, the menu content "settles" from a rotated/zoomed state into place, and the nav links rise into view with a stagger. Hovering each link cross-fades a stacked preview image with a scale + rotate entrance.

## Tech

Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm). No GSAP plugins are needed — only core tweens (`gsap.to` / `gsap.set`) animating transforms and `clipPath`.

```js
import gsap from "gsap";
```

Wrap all JS in `DOMContentLoaded`.

## Layout / HTML

```
<nav>
  <div class="logo"><a href="#">Void Construct</a></div>
  <div class="menu-toggle">
    <p id="menu-open">Menu</p>
    <p id="menu-close">Close</p>
  </div>
</nav>

<div class="menu-overlay">
  <div class="menu-content">
    <div class="menu-items">
      <div class="col-lg">
        <div class="menu-preview-img"><img src="./img-1.jpg" alt="" /></div>
      </div>
      <div class="col-sm">
        <div class="menu-links">
          <div class="link"><a href="#" data-img="./img-1.jpg">Visions</a></div>
          <div class="link"><a href="#" data-img="./img-2.jpg">Core</a></div>
          <div class="link"><a href="#" data-img="./img-3.jpg">Signals</a></div>
          <div class="link"><a href="#" data-img="./img-4.jpg">Connect</a></div>
        </div>
        <div class="menu-socials">
          <div class="social"><a href="#">Behance</a></div>
          <div class="social"><a href="#">Dribbble</a></div>
          <div class="social"><a href="#">LinkedIn</a></div>
          <div class="social"><a href="#">Instagram</a></div>
        </div>
      </div>
    </div>
    <div class="menu-footer">
      <div class="col-lg"><a href="#">Run Sequence</a></div>
      <div class="col-sm"><a href="#">Origin</a><a href="#">Join Signal</a></div>
    </div>
  </div>
</div>

<div class="container">
  <section class="hero">
    <div class="hero-img"><img src="./hero.jpg" alt="" /></div>
    <h1>Digital architecture that rises from the void.</h1>
  </section>
</div>
```

Note the stacking: `nav` sits above everything (`z-index: 2`), `.menu-overlay` above the page (`z-index: 1`), and `.container` holds the actual page (a fullscreen hero).

## Styling

- Reset: `* { margin: 0; padding: 0; box-sizing: border-box; }`. `body { overflow-x: hidden; }`.
- Font: `"TWK Lausanne", "Inter", sans-serif` — import Inter from Google Fonts as the working fallback.
- `img { width: 100%; height: 100%; object-fit: cover; }`.
- `h1`: color `#fff`, `font-size: 7rem`, `font-weight: 400`, `letter-spacing: -0.2rem`, `line-height: 1`. Inside `.hero`, the h1 is `width: 80%`.
- All `a, p`: `position: relative`, no underline, color `#fff`, `font-size: 1rem`, `font-weight: 300`, `user-select: none`. Logo link is `font-weight: 600`.
- `nav`: `position: fixed`, `width: 100vw`, `padding: 2.5em`, flex `space-between` + centered, `z-index: 2`.
- `.menu-toggle`: `position: relative`, `width: 3rem`, `height: 1.5rem`, `cursor: pointer`. Both `p` children are `position: absolute` with `transform-origin: top left` and `will-change: transform, opacity` (they stack on top of each other).
- `.menu-overlay`: `position: fixed`, `width: 100vw`, `height: 100svh`, background `#0f0f0f`, `z-index: 1`.
- `.menu-content`: `position: relative`, 100% width/height, flex centered, `transform-origin: left bottom`, `will-change: opacity, transform`.
- `.menu-items` and `.menu-footer`: `width: 100%`, `padding: 2.5em`, `display: flex`, `gap: 2.5em`. `.col-lg { flex: 3 }`, `.col-sm { flex: 2 }`. `.menu-footer` is absolutely positioned at `bottom: 0`; its `.col-sm` is flex `space-between`.
- `.menu-items .col-lg`: flex, centered — it holds `.menu-preview-img`: `position: relative`, `width: 45%`, `height: 100%`, `overflow: hidden`. Its `img`s are `position: absolute` (they stack for the cross-fade), `will-change: transform, opacity`.
- `.menu-items .col-sm`: `padding: 2.5em 0`, column flex, `gap: 2.5em`. `.menu-links` and `.menu-socials` are column flex with `gap: 0.5em`.
- `.link` and `.social` wrappers: `padding-bottom: 6px` and `clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%)` — this rectangle clip is the mask that hides the links when they are translated down.
- `.link a, .social a`: `display: inline-block`, `will-change: transform`, `transition: color 0.5s`. `.link a` is `font-size: 3.5rem`, `letter-spacing: -0.02rem`. `.social a` is `#8f8f8f` and turns `#fff` on hover.
- Underline hover on `.link a`, `.social a`, `.menu-footer a` via `::after`: absolute, `top: 102.5%`, `left: 0`, full width, `height: 2px`, white, `transform: scaleX(0)` with `transform-origin: right`, `transition: transform 0.3s cubic-bezier(0.6, 0, 0.4, 1)`; on hover `scaleX(1)` with `transform-origin: left` (draws left→right in, right→left out).
- `.container`: `position: relative`, 100% width/height, `transform-origin: right top`, `will-change: transform`.
- `.hero`: `position: relative`, `100vw × 100svh`, `padding: 2.5em`, flex with `align-items: flex-end`, `overflow: hidden`. `.hero-img`: absolute, top/left 0, `100% × 100svh`, `z-index: -1`.

### Initial (closed) states — set these in CSS

```css
.menu-toggle p#menu-close {
  opacity: 0;
  transform: translateX(-5px) translateY(10px) rotate(5deg);
}
.link a, .social a {
  transform: translateY(120%);
  opacity: 0.25;
}
.menu-content {
  transform: translateX(-100px) translateY(-100px) scale(1.5) rotate(-15deg);
  opacity: 0.25;
}
.menu-overlay {
  clip-path: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%); /* collapsed to zero height */
}
```

## GSAP effect (exhaustive)

State: two booleans, `isOpen = false` and `isAnimating = false`. Clicking `.menu-toggle` calls `openMenu()` if closed, `closeMenu()` if open. Both bail out if `isAnimating` is true (or if already in the target state).

### openMenu() — five parallel tweens, all fired at once

1. **Page container pushed away** — `gsap.to(".container", { rotation: 10, x: 300, y: 450, scale: 1.5, duration: 1.25, ease: "power4.inOut" })`. Because `.container` has `transform-origin: right top`, the page swings down-right and zooms.
2. **Toggle label swap** (shared helper, see below) — "Menu" out, "Close" in.
3. **Menu content settles** — `gsap.to(".menu-content", { rotation: 0, x: 0, y: 0, scale: 1, opacity: 1, duration: 1.25, ease: "power4.inOut" })` — from its CSS initial state `(-100, -100, scale 1.5, -15deg, opacity 0.25)` to identity. With `transform-origin: left bottom` it reads as the content rotating/zooming into place.
4. **Links + socials rise** — `gsap.to([".link a", ".social a"], { y: "0%", opacity: 1, delay: 0.75, duration: 1, stagger: 0.1, ease: "power3.out" })`. They come up from `y: 120%` behind the wrappers' clip-path masks. Single flat stagger across all 8 anchors (4 links then 4 socials), 0.1s apart.
5. **Overlay clip-path opens** — `gsap.to(".menu-overlay", { clipPath: "polygon(0% 0%, 100% 0%, 100% 175%, 0% 100%)", duration: 1.25, ease: "power4.inOut" })`. The bottom-right corner overshoots to 175%, so while opening the bottom edge is diagonal/skewed (the overlay wipes down with a slanted edge). `onComplete`: set `isOpen = true`, `isAnimating = false`.

### closeMenu() — mirror of open, all fired at once

1. `gsap.to(".container", { rotation: 0, x: 0, y: 0, scale: 1, duration: 1.25, ease: "power4.inOut" })`.
2. Toggle label swap — "Close" out, "Menu" in.
3. `gsap.to(".menu-content", { rotation: -15, x: -100, y: -100, scale: 1.5, opacity: 0.25, duration: 1.25, ease: "power4.inOut" })`.
4. `gsap.to(".menu-overlay", { clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", duration: 1.25, ease: "power4.inOut" })` — collapses back to the zero-height polygon. `onComplete`: `isOpen = false`, `isAnimating = false`, then `gsap.set([".link a", ".social a"], { y: "120%" })` (instantly re-hide the links for the next open — only `y` is reset, not opacity), and reset the preview container back to a single default `img-1` (`menuPreviewImg.innerHTML = ""` then append a fresh `<img src="./img-1.jpg">`).

Note: the links are NOT animated out during close — the overlay clip-path and content transform hide them; they are snapped back down only after the close completes.

### Toggle label swap helper — `animateMenuToggle(isOpening)`

Two tweens on the stacked `p#menu-open` / `p#menu-close`:

- Outgoing label (`#menu-open` when opening, `#menu-close` when closing): `gsap.to(el, { x: -5, y: isOpening ? -10 : 10, rotation: isOpening ? -5 : 5, opacity: 0, delay: 0.25, duration: 0.5, ease: "power2.out" })` — it drifts up-left with a small counter-rotation when opening, down-left with positive rotation when closing.
- Incoming label: `gsap.to(el, { x: 0, y: 0, rotation: 0, opacity: 1, delay: 0.5, duration: 0.5, ease: "power2.out" })` — returns to identity from wherever the previous cycle left it (the CSS initial state gives `#menu-close` its offscreen pose on first load).

### Hover image preview (link → stacked image cross-fade)

For every `.link a`, on `mouseover`:

- Ignore unless `isOpen && !isAnimating`.
- Read the link's `data-img`. Bail if the LAST `<img>` currently inside `.menu-preview-img` already has that src (compare with `src.endsWith(imgSrc)`), so re-hovering the same link does nothing.
- Create a new `<img>` with that src, give it inline starting styles `opacity: 0` and `transform: scale(1.25) rotate(10deg)`, and append it on top of the stack inside `.menu-preview-img`.
- Cleanup: if the container now holds more than 3 images, remove the oldest ones so at most 3 remain (old images just sit underneath — no exit animation).
- Animate the new image in: `gsap.to(newImg, { opacity: 1, scale: 1, rotation: 0, duration: 0.75, ease: "power2.out" })`.

## Assets / images

5 photographs, all `object-fit: cover` so exact ratios are forgiving:

- `hero.jpg` — 1 moody, dark architectural/editorial photo, full-bleed landscape (~16:9), used as the fullscreen hero background behind the big white headline.
- `img-1.jpg` … `img-4.jpg` — 4 atmospheric editorial photos in the same dark visual family, displayed in a portrait-ish frame (the preview box is 45% of the left column's width and full height, roughly 3:4). `img-1` doubles as the default preview shown when the menu opens; each of the 4 links maps to one image via `data-img`.

## Behavior notes

- Under `max-width: 900px`: hero h1 becomes `4rem`, full width, `letter-spacing: 0`; the preview column (`.menu-items .col-lg`) is hidden entirely; the `::after` hover underlines are disabled.
- The overlay stays permanently in the DOM covering the viewport — it is only ever hidden by its collapsed clip-path, so no `display` toggling.
- The `isAnimating` lock prevents re-triggering the toggle and hover swaps mid-transition; all open/close tweens share the same `1.25s` / `power4.inOut` signature so everything moves as one gesture.
- No scroll behavior, no ScrollTrigger, no smooth-scroll library — this is a click/hover-driven, page-level component.
