# Circular Futuristic Navigation Menu — Radial Segments + Draggable Joystick

## Goal
Build a fullscreen overlay navigation menu whose 6 links are arranged as **radial "pie-donut" segments** (clip-path wedges) around a **draggable white joystick** in the center. Clicking a rounded hamburger tab at the bottom of the screen toggles the overlay: the joystick **pops in with a back-out scale**, and the nav bar, footer and every wedge **flicker in with randomized glitchy yoyo blinks** (GSAP `repeat`/`yoyo` opacity pulses). While open, hovering a wedge — or **dragging the joystick toward it** — triggers a CSS "flicker to solid white" keyframe animation on that wedge, like a sci-fi console selection. Short UI sound effects play on open/close/select. The star effect is the combination of the randomized glitch-flicker reveal and the lerped joystick-drag segment highlighting.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) — **core only, no plugins**. No smooth-scroll library. Icons come from **Ionicons v7 web components** loaded via CDN script tags in the HTML:

```html
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
```

Ship `index.html`, `styles.css`, and an ES-module `script.js` (`<script type="module" src="./script.js">`).

## Layout / HTML
The wedge segments are generated by JS at runtime; the static skeleton is:

```html
<div class="menu-toggle-btn">
  <div class="hamburger-bar"></div>
  <div class="hamburger-bar"></div>
</div>

<div class="menu-overlay">
  <div class="menu-bg"></div>

  <div class="menu-overlay-nav">
    <div class="close-btn">
      <div class="close-btn-bar"></div>
      <div class="close-btn-bar"></div>
    </div>
    <div class="menu-overlay-items">
      <a href="#"><ion-icon name="logo-google"></ion-icon></a>
      <a href="#"><ion-icon name="logo-github"></ion-icon></a>
      <a href="#"><ion-icon name="logo-vercel"></ion-icon></a>
    </div>
  </div>

  <div class="menu-overlay-footer">
    <p>Copyright &copy; 2025 All Rights Reserved</p>
    <div class="menu-overlay-items">
      <a href="#">Cookie Settings</a>
      <a href="#">Privacy Policy</a>
      <a href="#">Legal Disclaimer</a>
    </div>
  </div>

  <div class="circular-menu">
    <div class="joystick">
      <ion-icon name="grid-sharp" class="center-icon center-main"></ion-icon>
      <ion-icon name="chevron-up-sharp" class="center-icon center-up"></ion-icon>
      <ion-icon name="chevron-down-sharp" class="center-icon center-down"></ion-icon>
      <ion-icon name="chevron-back-sharp" class="center-icon center-left"></ion-icon>
      <ion-icon name="chevron-forward-sharp" class="center-icon center-right"></ion-icon>
    </div>
  </div>
</div>
```

The 6 menu items (label / ionicon name / href) that the JS turns into wedges:

```js
const menuItems = [
  { label: "Vision",    icon: "scan-sharp",        href: "#vision" },
  { label: "Portfolio", icon: "layers-sharp",      href: "#portfolio" },
  { label: "People",    icon: "person-sharp",      href: "#people" },
  { label: "Insights",  icon: "browsers-sharp",    href: "#insights" },
  { label: "Careers",   icon: "stats-chart-sharp", href: "#careers" },
  { label: "About Us",  icon: "reader-sharp",      href: "#about" },
];
```

## Styling

**Global / typography**
- `* { margin: 0; padding: 0; box-sizing: border-box; }`
- `body { font-family: "Host Grotesk"; background: #000; }` — import **Host Grotesk** and **IBM Plex Mono** from Google Fonts.
- `p, a { color: #fff; text-decoration: none; text-transform: uppercase; font-family: "IBM Plex Mono"; font-size: 11px; }`

**Bottom toggle tab** — a big white half-dome peeking up from the bottom edge:
- `.menu-toggle-btn`: `position: fixed; bottom: -6rem; left: 50%; transform: translateX(-50%); width: 25rem; height: 10rem; padding-top: 2rem; border-radius: 50% 50% 0 0 / 90% 90% 0 0; background-color: #fff; color: #000; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; gap: 0.25rem; cursor: pointer;` — only its top ~4rem arc is visible.
- `.hamburger-bar`: `width: 2rem; height: 0.125rem; background-color: #000;` (two of them, stacked with the 0.25rem gap).

**Overlay**
- `.menu-overlay`: `position: fixed; top: 0; left: 0; width: 100vw; height: 100svh; display: flex; justify-content: center; align-items: center; overflow: hidden; z-index: 100;` and, crucially, initial state `opacity: 0; pointer-events: none;` in the CSS.
- `.menu-bg`: `position: absolute; width: 100%; height: 100%;` with the backdrop image as `background: url(...) no-repeat 50% 50%; background-size: cover;`.

**Overlay nav & footer**
- `.menu-overlay-nav, .menu-overlay-footer`: `position: absolute; width: 100vw; padding: 1.5rem; display: flex; justify-content: space-between; align-items: center;` — nav pinned `top: 0`, footer `bottom: 0`.
- `.close-btn`: `position: relative; width: 1.5rem; height: 1.5rem; cursor: pointer;` with two `.close-btn-bar`s: `position: absolute; top: 50%; width: 1.5rem; height: 0.125rem; background-color: #fff;`, first rotated `45deg`, second `-45deg` (an X).
- `.menu-overlay-items { display: flex; gap: 1rem; }`; the nav's icon links get `font-size: 18px`.

**Circular menu & joystick**
- `.circular-menu`: `position: relative; width: 600px; height: 600px; z-index: 10;` (JS overrides the size at runtime).
- `.joystick`: `position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100px; height: 100px; background: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; user-select: none; cursor: grab; z-index: 100;`
- `.center-icon`: `position: absolute; color: #000; font-size: 12px;`. `.center-main` is `font-size: 30px` (centered by the flex). The four chevrons sit at the pad edges: `.center-up { top: 0.75rem; left: 50%; transform: translateX(-50%); }`, `.center-down { bottom: 0.75rem; ... }`, `.center-left { left: 0.75rem; top: 50%; transform: translateY(-50%); }`, `.center-right { right: 0.75rem; ... }` — it reads like a game-controller D-pad.

**Wedge segments (JS-created)**
- `.menu-segment`: `position: absolute; width: 100%; height: 100%; color: #fff; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(20px); cursor: pointer;` — the wedge shape itself comes from an inline `clip-path: path(...)` set by JS.
- `.segment-content`: `position: absolute; display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: 600; text-align: center;` — its `ion-icon` is `font-size: 40px; margin-bottom: 10px;` (drops to `20px` at `max-width: 1000px`).
- `.label`: `text-transform: none; font-family: "Host Grotesk"; font-size: 15px; font-weight: 500;`

**Hover flicker (pure CSS keyframes — also reused by the joystick via inline styles)**
- `.menu-segment:hover { animation: flickerHover 350ms ease-in-out forwards; z-index: 10; }`
- `.menu-segment:hover .segment-content { animation: contentFlickerHover 350ms ease-in-out forwards; }`
- `@keyframes flickerHover` alternates the wedge between translucent-blurred and near-solid white, converging on solid: `0% { background: rgba(255,255,255,0.05); backdrop-filter: blur(20px); }` → `12% { rgba .7 / blur 8px }` → `24% { rgba .15 / blur 18px }` → `36% { rgba .85 / blur 5px }` → `48% { rgba .25 / blur 15px }` → `60% { rgba .9 / blur 3px }` → `72% { rgba .3 / blur 12px }` → `84% { rgba .95 / blur 2px }` → `100% { background: rgba(255,255,255,1); backdrop-filter: blur(0px); }`.
- `@keyframes contentFlickerHover` flickers the icon+label between white and dark while it strobes, ending black on the now-white wedge: `0% { color: white; opacity: 1 }` → `12% { color: #333; opacity: .4 }` → `24% { white / .9 }` → `36% { #333 / .3 }` → `48% { white / .8 }` → `60% { #333 / .2 }` → `72% { white / .7 }` → `84% { #333 / .1 }` → `100% { color: #000; opacity: 1 }`.

## GSAP effect (exhaustive)

GSAP core only — everything is tween-based (`gsap.to` / `gsap.set`); no ScrollTrigger, no SplitText, no timelines. State flags: `isOpen` (starts `false`) and `isMenuAnimating` (re-entry guard: `toggleMenu` returns early while `true`).

### 1) Responsive sizing + wedge geometry (runs on `DOMContentLoaded`)

Compute a config from the viewport:
- `isMobile = window.innerWidth < 1000`
- `maxSize = Math.min(viewportWidth * 0.9, viewportHeight * 0.9)`
- `menuSize = isMobile ? Math.min(maxSize, 480) : 700`
- `center = menuSize / 2`, `innerRadius = menuSize * 0.08`, `outerRadius = menuSize * 0.42`, `contentRadius = menuSize * 0.28`

Set `.circular-menu`'s width/height to `menuSize` px. Then for each of the 6 items build an `<a class="menu-segment" href="...">` sized `menuSize × menuSize` and clipped to an annular wedge with `clip-path: path('...')`:
- `anglePerSegment = 360 / 6 = 60`; `baseStartAngle = 60 * index`; `centerAngle = baseStartAngle + 30`.
- Leave a hairline gap between wedges: `startAngle = baseStartAngle + 0.19`, `endAngle = baseStartAngle + 60 - 0.19`.
- Angles are measured from 12 o'clock, so every trig call uses `(angle - 90) * Math.PI / 180`. Compute the 4 corner points at `innerRadius`/`outerRadius` for `startAngle`/`endAngle` (x = `center + r*cos`, y = `center + r*sin`).
- Path data: `M innerStart` → `L outerStart` → `A outerRadius outerRadius 0 largeArc 1 outerEnd` → `L innerEnd` → `A innerRadius innerRadius 0 largeArc 0 innerStart` → `Z` (with `largeArc = endAngle - startAngle > 180 ? 1 : 0`, i.e. 0 here).
- Inject the content at the wedge's angular bisector: a `.segment-content` absolutely positioned at `left = center + contentRadius*cos((centerAngle-90)·π/180)`, `top = center + contentRadius*sin(...)`, with inline `transform: translate(-50%, -50%)`, containing `<ion-icon name="...">` and `<div class="label">…</div>`.
- Segment `mouseenter` → play the "select" sound only if `isOpen` (always `.catch(() => {})` the play promise).

Initial GSAP states: `gsap.set(joystick, { scale: 0 })` and `gsap.set([nav, footer], { opacity: 0 })`. Wire `click` on both `.menu-toggle-btn` and `.close-btn` to `toggleMenu`.

### 2) OPEN sequence (`toggleMenu` when closed)

Set `isMenuAnimating = true`, `isOpen = true`, play the "open" sound, then fire these tweens in parallel:

1. **Overlay fade-in**: `gsap.to('.menu-overlay', { opacity: 1, duration: 0.3, ease: "power2.out", onStart: () => overlay.style.pointerEvents = "all" })`.
2. **Joystick pop**: `gsap.to('.joystick', { scale: 1, duration: 0.4, delay: 0.2, ease: "back.out(1.7)" })` — an overshooting spring from scale 0.
3. **Nav + footer glitch-in**: first `gsap.set([nav, footer], { opacity: 0 })`, then `gsap.to([nav, footer], { opacity: 1, duration: 0.075, delay: 0.3, repeat: 3, yoyo: true, ease: "power2.inOut", onComplete: () => gsap.set([nav, footer], { opacity: 1 }) })`. The odd repeat count + yoyo would end at opacity 0, hence the forced `set` to 1 on complete — that final snap is part of the glitch look.
4. **Segment flicker cascade, in RANDOM order**: shuffle the indices `[0..5]` (e.g. `[...Array(n).keys()].sort(() => Math.random() - 0.5)`). For each `originalIndex` at `shuffledPosition`:
   - `gsap.set(segment, { opacity: 0 })`
   - `gsap.to(segment, { opacity: 1, duration: 0.075, delay: shuffledPosition * 0.075, repeat: 3, yoyo: true, ease: "power2.inOut", onComplete: ... })` — on complete, `gsap.set(segment, { opacity: 1 })`, and if `originalIndex === 5` (the last item's index, not the last shuffled slot) set `isMenuAnimating = false`.
   - Net effect: each wedge blinks on/off ~4 times over 0.3s, wedges starting 75ms apart in a random sequence — a hexagonal HUD booting up.

### 3) CLOSE sequence (`toggleMenu` when open)

Set `isMenuAnimating = true`, `isOpen = false`, play the "close" sound, then in parallel:

1. **Nav + footer glitch-out**: `gsap.to([nav, footer], { opacity: 0, duration: 0.05, repeat: 2, yoyo: true, ease: "power2.inOut", onComplete: () => gsap.set([nav, footer], { opacity: 0 }) })`.
2. **Joystick shrink**: `gsap.to('.joystick', { scale: 0, duration: 0.3, delay: 0.2, ease: "back.in(1.7)" })`.
3. **Segments flicker out in a fresh random order**: for each shuffled segment `gsap.to(segment, { opacity: 0, duration: 0.05, delay: shuffledPosition * 0.05, repeat: 2, yoyo: true, ease: "power2.inOut", onComplete: () => gsap.set(segment, { opacity: 0 }) })` — faster and snappier than the open (50ms blinks, 50ms stagger).
4. **Overlay fade-out, last**: `gsap.to('.menu-overlay', { opacity: 0, duration: 0.3, delay: 0.6, ease: "power2.out", onComplete: () => { overlay.style.pointerEvents = "none"; isMenuAnimating = false; } })`.

### 4) Joystick drag + lerp + segment targeting (rAF loop)

A persistent `requestAnimationFrame` loop drives the joystick with **lerp factor 0.15**:

- State: `isDragging`, `currentX/Y` (rendered), `targetX/Y` (goal), `activeSegment`.
- Every frame: `currentX += (targetX - currentX) * 0.15` (same for Y), then `gsap.set(joystick, { x: currentX, y: currentY })`.
- **mousedown** on the joystick: record its center from `getBoundingClientRect()`, then on document `mousemove` compute `deltaX/deltaY` from that center and `distance = √(dx²+dy²)`:
  - dead zone: `distance <= 20` → `targetX = targetY = 0`;
  - clamp: max drag radius is `25px` (`100 * 0.25`); if `distance > 25`, scale the delta by `25 / distance`;
  - otherwise target the raw delta. Call `e.preventDefault()`.
- **mouseup**: stop dragging, reset targets to `0,0` — the knob **springs back to center via the lerp** (no tween).
- **Segment targeting** (inside the rAF loop): while dragging and `√(currentX² + currentY²) > 20`, compute `angle = atan2(currentY, currentX) * 180/π` and `segmentIndex = floor(((angle + 90 + 360) % 360) / 60) % 6` (the `+90` maps atan2's 3-o'clock zero to the menu's 12-o'clock start). If that segment differs from `activeSegment`: clear the previous one's inline `animation` (on both the wedge and its `.segment-content`) and `z-index`, then set on the new one — inline — `animation: "flickerHover 350ms ease-in-out forwards"` on the wedge, `animation: "contentFlickerHover 350ms ease-in-out forwards"` on its content, `z-index: 10`, and play the "select" sound (if `isOpen`). When the knob returns inside the dead zone (or drag ends), clear the active segment's inline animation styles so it reverts to translucent.

## Assets / images
- **1 background image, landscape 16:9 (~1440×810 or larger)** — a full-bleed backdrop for the open overlay: a bold, glossy, high-contrast hero visual on a black/dark background (any striking centered subject works). It sits behind the translucent blurred wedges, so it should have strong color/contrast for the `backdrop-filter` to read.
- **3 short UI sound effects (mp3, < 1s each)**: an "open" whoosh/click, a "close" variant, and a "select" blip for segment highlighting. Treat them as optional: create them or skip them, but **always wrap `Audio.play()` in `.catch(() => {})`** (autoplay policies and missing files must never throw console errors).

## Behavior notes
- The overlay is non-interactive (`pointer-events: none`) until opened; the toggle tab stays visible beneath it (overlay has `z-index: 100`).
- Menu size is responsive: 700px desktop, `min(90vmin, 480px)` under 1000px viewport width; segment icons shrink from 40px to 20px at the same breakpoint. Sizing is computed once on load (no resize listener needed).
- The drag interaction is mouse-only (desktop-oriented); hover flicker still works as a fallback everywhere.
- The `isMenuAnimating` flag must block re-toggling until the open/close choreography finishes.
- No infinite loops besides the rAF lerp; no scroll behavior at all — the component is a self-contained fixed-position widget over a black page.
