# Circular Image Gallery — Radial Ring with Click-to-Focus

## Goal
Build a full-viewport, minimal editorial page where, **on load**, 15 small portrait image tiles **fly out from the exact center of the screen and arrange themselves into a perfect circle** — each tile scaling up from nothing, sliding to its spot on the ring, and rotating so it sits like a petal/clock-numeral along its radial spoke, all with a staggered decelerating entrance. **Clicking any tile** makes every other tile shrink away to nothing and the chosen tile glides back to the center and **scales up 5×** to become a hero image; **clicking it again** sends it back to its ring position and pops all the others back in. The star effect is this GSAP-timeline **radial fan-out** plus the **click-to-focus zoom / restore**.

## Tech
Vanilla HTML/CSS/JS with ES-module imports. Use **`gsap`** (npm) only — **no plugins, no ScrollTrigger, no smooth-scroll, no other libraries**. Ship one `index.html`, one `styles.css`, one ES-module `script.js` (`<script type="module" src="./script.js">`). It must run in a fresh Vite + npm project. `import gsap from "gsap";` is the single import.

## Layout / HTML
Dead-simple, static skeleton — the JS injects the `<img>` into each tile at runtime, so the markup ships **15 empty `.item` divs**:

```html
<body>
  <nav>
    <p>Motionprompts &nbsp;&nbsp;&nbsp;&nbsp; / &nbsp;&nbsp;&nbsp;&nbsp;26092017</p>
    <p>Youtube</p>
  </nav>
  <footer>
    <p>Subscribe</p>
    <p>Motionprompts</p>
  </footer>
  <div class="container">
    <div class="gallery">
      <div class="item"></div>
      <!-- exactly 15 identical .item divs -->
    </div>
  </div>
  <script type="module" src="./script.js"></script>
</body>
```

- `nav` / `footer` — fixed UI chrome pinned to the top and bottom edges.
- `.container` — the full-screen stage the tiles live in; **`overflow: hidden`** so tiles that scale up 5× are clipped to the viewport.
- `.gallery > .item ×15` — the tiles. Each starts stacked at the dead center of the container; JS gives each one an `<img>` and animates it out onto the ring.

## Styling

**Reset:** `* { margin:0; padding:0; box-sizing:border-box; }`

**Page / typography**
```css
html, body { width:100%; height:100vh; font-family:"Timmons NY", sans-serif; }
```
Use a **bold condensed display / grotesque font** for the nav & footer (fall back to `sans-serif`). All chrome text is **uppercase**.

**Fixed chrome (nav + footer)**
```css
nav, footer {
  position:fixed; width:100%;
  display:flex; justify-content:space-between;
  padding:1em; text-transform:uppercase; font-size:30px;
}
nav    { top:0; }
footer { bottom:0; }
```
Two lines each, pushed to the far left and far right (space-between). Neutral placeholder copy — no real brand marks.

**Stage**
```css
.container { position:relative; width:100%; height:100%; overflow:hidden; }
```

**Tile**
```css
.item {
  position:absolute;
  top:50%; left:50%;
  transform:translate(-50%,-50%);   /* every tile starts centered */
  width:70px; height:100px;         /* portrait 7:10 */
  background:#b0b0b0;               /* grey placeholder shown until the img paints */
  margin:10px;
}
img { width:100%; height:100%; object-fit:cover; }  /* fills the tile, center-cropped */
```
Key facts the effect depends on: tiles are **absolutely positioned, all initially anchored at `top:50% / left:50%` with a `translate(-50%,-50%)`** (so they overlap in a stack at the center before animating), they are **70×100px portrait** rectangles, and the grey `#b0b0b0` is just the pre-load fill behind each image.

No page background color is set (default white). There is no other styling — the ring of photos *is* the page.

## The GSAP effect (be exhaustive — this is the whole component)

Everything runs from a single `window.onload` handler. There is **one entrance timeline** on load and **imperative `gsap.to()` tweens** on click. Reproduce the constants, geometry and tween params exactly.

### Setup constants
```js
const items          = document.querySelectorAll('.item');   // 15 tiles
const container      = document.querySelector('.container');
const numberOfItems  = items.length;                         // 15
const angleIncrement = (2 * Math.PI) / numberOfItems;        // even angular spacing around the full circle
const radius         = 300;                                  // ring radius in px
let   isGalleryOpen  = false;                                // guards click state

const centerX = container.offsetWidth  / 2;                  // pixel center of the stage
const centerY = container.offsetHeight / 2;

const tl = gsap.timeline();                                  // the entrance timeline
```

### Per-tile geometry (computed in a `forEach(item, index)` loop)
For each tile, **inject its image** (`const img = document.createElement('img'); img.src = <indexed path, e.g. img{index+1}.jpg>; item.appendChild(img);`), then compute its resting place on the ring:
```js
const angle           = index * angleIncrement;               // 0 → 2π across the 15 tiles
const initialRotation = (angle * 180 / Math.PI) - 90;         // tile's tilt IN DEGREES: radian angle → deg, minus 90
const x = centerX + radius * Math.cos(angle);                 // target left (px) on the circle
const y = centerY + radius * Math.sin(angle);                 // target top  (px) on the circle
```
The `−90°` rotation makes each tile's long (vertical) axis line up with its **radial spoke**, so the ring reads like petals / the numerals on a clock face fanning around the center.

### Entrance animation (on load) — the star moment
Before animating, hard-set every tile to invisible size: `gsap.set(item, { scale: 0 });`. Then add each tile's tween to the shared timeline **at an explicit position** so they cascade:
```js
tl.to(item, {
  left: x + 'px',              // 50% (center) → its ring X
  top:  y + 'px',              // 50% (center) → its ring Y
  rotation: initialRotation,   // 0 → its radial tilt
  scale: 1,                    // 0 → 1 (grows in)
  duration: 1,
  ease: "power2.out",
  delay: 1,                    // each tween also waits 1s
}, index * 0.1);               // ← timeline position param = 0.1s stagger between tiles
```
Critical details:
- The **stagger is produced by the position parameter `index * 0.1`** (not GSAP's `stagger` option): tile 0 is placed at t=0, tile 1 at t=0.1, … tile 14 at t=1.4 on the timeline.
- Each tween **additionally** carries `delay: 1`, so the first tile actually starts moving at ~1s and the last finishes at ~`1.4 + 1(delay) + 1(dur)` ≈ **3.4s**. Budget ~3s before the ring is settled.
- All four properties animate together per tile: it **scales 0→1, slides from screen-center to its (x,y) on the ring, and rotates 0→`initialRotation`**, on a `power2.out` (fast start, soft settle). Net look: tiles burst out of the middle and spin into a clean circle one after another.

### Click a tile → focus / zoom to center
Attach a `click` listener to each tile (inside the same loop, so it closes over that tile's `x`, `y`, `initialRotation`). On click, **only if `!isGalleryOpen`**:
1. Set `isGalleryOpen = true`.
2. **Clone the clicked tile**: `const duplicate = item.cloneNode(true); duplicate.style.position = 'absolute'; container.appendChild(duplicate);` — the original and this duplicate are animated together as a pair for the rest of the focus sequence.
3. **Shrink all the other tiles away**:
   ```js
   gsap.to(Array.from(items).filter(i => i !== item), {
     scale: 0, duration: 0.5, ease: "power2.in", stagger: 0.05
   });
   ```
4. **Normalize the rotation** so the focus move takes the short way round, then snap it instantly:
   ```js
   const endRotation = initialRotation > 180 ? initialRotation - 360 : initialRotation;
   gsap.to([item, duplicate], {
     rotation: endRotation,
     duration: 0.0001,             // effectively instant
     onComplete: () => {
       gsap.to([item, duplicate], {
         left: "50%",
         top:  "50%",
         transform: "translate(-50%, -50%) scale(5)",  // ← animated as a raw CSS transform string, scale ×5
         duration: 1,
         ease: "power2.out",
         delay: 1.25               // waits 1.25s (lets the others finish collapsing) before flying in
       });
     }
   });
   ```
   So after a ~1.25s beat the chosen tile (and its clone, stacked on top) **glides back to dead-center and scales up 5×**, filling the stage as a hero image (clipped by the container's `overflow:hidden`).

### Click again → restore the ring
Define a `closeGallery` handler and attach it to **both** the original tile and the duplicate (`item.addEventListener('click', closeGallery); duplicate.addEventListener('click', closeGallery);`). On this second click, if `isGalleryOpen`:
```js
gsap.to([item, duplicate], {
  left: x + 'px',                 // back to its ring X
  top:  y + 'px',                 // back to its ring Y
  scale: 1,                       // 5 → 1
  rotation: initialRotation,      // back to its radial tilt
  duration: 1,
  ease: "power2.out",
  onComplete: () => {
    duplicate.remove();           // discard the clone
    gsap.to(items, {              // pop every tile back in
      scale: 1, duration: 1, stagger: 0.05, ease: "power2.out"
    });
    isGalleryOpen = false;        // re-arm for the next click
  }
});
```
Net: the hero tile shrinks and travels back to its slot on the ring, the clone is removed, and all the collapsed tiles scale `0→1` back into place with a 0.05s stagger.

### Motion summary
- **Entrance:** `power2.out`, per-tile `duration:1`, `delay:1`, positioned at `index*0.1` → radial burst that settles in ~3.4s.
- **Collapse others:** `power2.in`, `duration:0.5`, `stagger:0.05`.
- **Focus in:** `power2.out`, `duration:1`, `delay:1.25`, scale ×5 to center.
- **Restore:** `power2.out`, `duration:1`; then others back with `stagger:0.05`, `duration:1`, `power2.out`.

## Assets / images
**15 images**, each filling the same role — one photo per tile in the ring, injected via an indexed path pattern (`…/img1.jpg` … `…/img15.jpg`). Displayed in a **70×100px portrait tile (~7:10, close to 2:3)** with `object-fit:cover`, so they are **center-cropped** and, in focus mode, scaled up 5× to roughly fill the screen — so the central subject and tonal contrast matter more than resolution.

Curate an **eclectic but tonally cohesive, moody art-directed editorial set**: a mix of cinematic figures (a lone silhouette walking toward light between towering slabs; an astronaut before a giant pale sphere in golden haze; a hand pressed to backlit frosted glass), fashion/beauty portraits (low-key studio headshots, a face behind a pale tulle veil, oversized futuristic wraparound sunglasses on a warm ground), surreal 3D renders (a chrome robotic figure in mirrored shades, a smooth faceless figure lit green), still lifes (red roses on black; a golden lucky-cat figurine in a red niche), and atmospheric landscapes (a hazy sunset skyline; a figure on calm water at dusk). Dominant palette: deep teals, warm oranges/golds, moody blues, greys and blacks with occasional saturated accents. Portrait, center-croppable framing. **No real brand imagery or client logos.**

## Behavior notes
- **Trigger model:** entrance is on `window.onload`; focus/restore is **click** (toggle). No scroll, hover, mousemove, or keyboard interaction; no ScrollTrigger.
- **Single-focus lock:** `isGalleryOpen` guards the state — while one tile is focused, clicking other tiles does nothing until you click the focused tile (or its clone) to restore.
- **Geometry is measured at load** from `container.offsetWidth/offsetHeight`; the ring is not recomputed on resize (no resize handler).
- **The clone quirk is intentional:** the focused tile is duplicated and the pair is animated together; the clone is removed only on restore. Reproduce it as-is.
- No reduced-motion branch, no infinite loops or timers — motion only occurs on load and on click. Lightweight and mobile-safe (15 tiles, no WebGL/canvas).
