# Draggable Timeline Horizontal Scroll — 500vw Editorial Track Panned by a Bottom Scrubber

## Goal
Build a **full-screen, black editorial fashion gallery** whose content lives on one enormous **500vw horizontal track** (five side-by-side full-viewport sections that alternate text and image spreads). The page itself never scrolls. Instead a **draggable "timeline" scrubber pinned to the bottom** of the screen — sitting over a ruler of thin vertical tick-marks — is dragged left/right, and that drag **pans the whole track horizontally**. The star effect: as you drag the scrubber, its position is normalized to a 0→1 progress that maps onto a **0 → −400vw** pan, and the track **eases to that target with a trailing, momentum-style `power3.out` tween** (each drag frame re-fires a 1s tween, so the track glides and lags smoothly behind your hand rather than snapping). Built with GSAP **Draggable**.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the single GSAP plugin **`Draggable`**. No ScrollTrigger, no smooth-scroll library, no canvas/WebGL. Import as:
```js
import gsap from "gsap";
import { Draggable } from "gsap/Draggable";   // or "gsap/all"
gsap.registerPlugin(Draggable);
```

## Layout / HTML
```
nav                                  (fixed strip across the top)
  a  "Urban Eclipse"                 (fictional brand / logo)
  a  "About"
  a  "Contact"
  a  "Work"

.container                           (the 500vw horizontal track — flex row of 5 sections)
  section#section-1                  (TEXT spread: heading + paragraph)
    h1  <long editorial headline>
    p   <two editorial paragraphs, separated by <br><br>>
  section#section-2                  (IMAGE spread: 3 images)
    .img.img-1 > img                 (wide slot, flex:2)
    .img.img-2 > img                 (narrow slot, flex:1)
    .img.img-3 > img                 (narrow slot, flex:1)
  section#section-3                  (IMAGE spread: 3 images)
    .img.img-4 > img                 (narrow, flex:1)
    .img.img-5 > img                 (wide,   flex:2)
    .img.img-6 > img                 (narrow, flex:1)
  section#section-4                  (TEXT spread: heading + paragraph)
    h1
    p
  section#section-5                  (IMAGE spread: 3 images)
    .img.img-7 > img                 (narrow, flex:1)
    .img.img-8 > img                 (wide,   flex:2)
    .img.img-9 > img                 (narrow, flex:1)

.timeline                            (fixed scrubber rail across the bottom)
  .scroller > p                      (the draggable handle)  "[<span>Drag</span>]"
  (+ 50 .marker tick divs injected by JS)
```
All copy is fictional/neutral — brand **"Urban Eclipse"**, nav links **About / Contact / Work**, and long dystopian-fashion editorial paragraphs. No real brand names.

## Styling
Global reset: `* { margin:0; padding:0; box-sizing:border-box; }`. `html, body { width:100%; height:100%; background:#000; color:#fff; }`. `img { width:100%; height:100%; object-fit:cover; }`.

**Palette:** pure black `#000` background, white `#fff` text/markers. Nothing else.

**Fonts** (the original uses two proprietary faces — name them with sane web fallbacks):
- **"Akkurat Mono"** (monospace) for the nav links and the scrubber label — any clean monospace substitute is fine. Nav links `a`: `text-decoration:none; color:#fff; text-transform:uppercase; font-size:12px;`. Scrubber `span`: `font-size:13px; padding:0 3em; text-transform:uppercase;`.
- **"PP Neue Montreal"** (a neutral neo-grotesque sans) for `h1` and `p` — substitute Inter / Neue Haas / any clean grotesque. `h1 { width:50%; font-weight:400; font-size:40px; text-transform:uppercase; }`, `p { width:40%; font-weight:400; font-size:16px; }`.

**Load-bearing structural CSS** (the JS reads pixel widths off these, so keep the geometry):
- `nav { position:fixed; top:0; width:100vw; padding:2em; display:flex; justify-content:space-between; }`.
- `.container { position:absolute; top:0; left:0; width:500vw; height:90vh; display:flex; }` — the single wide track. (**This is the element GSAP translates on `x`.**)
- `section { position:relative; width:100vw; height:100%; padding:6em 2em 0 2em; display:flex; gap:2em; overflow:hidden; }` — each section is exactly one viewport wide.
- Text sections only: `#section-1, #section-4 { display:flex; justify-content:space-between; }` (heading pinned left, paragraph pinned right).
- `.img { width:100%; height:100%; }`. Image sizing is done purely by flex within the section row: `.img-1, .img-5, .img-8 { flex:2; }` (the wide slot) and `.img-2,.img-3,.img-4,.img-6,.img-7,.img-9 { flex:1; }` (narrow slots). So each image section is a `wide | narrow | narrow` or `narrow | wide | narrow` composition, all `90vh` tall, hard-cropped with `object-fit:cover`.
- `.timeline { position:fixed; bottom:0; left:0; width:100vw; height:10vh; padding:2.25em 1em; display:flex; justify-content:space-around; }` — the bottom rail; `justify-content:space-around` evenly distributes the 50 tick markers across the full width.
- `.marker { width:1px; height:100%; background:#fff; }` — a thin vertical ruler tick (50 of them, injected by JS).
- `.scroller { position:absolute; top:50%; left:0; transform:translate(0%, -50%); background:#000; cursor:pointer; text-transform:uppercase; line-height:120%; }` — the draggable handle; a small black `[ Drag ]` pill floating over the ruler, vertically centered in the 10vh rail. `font-family` mono.

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

Everything runs once inside `window.onload` (so all fonts/layout are final and `offsetWidth` reads are correct).

### 1. Measure the rail
```js
const timeline   = document.querySelector(".timeline");
const scroller   = document.querySelector(".scroller");
const container  = document.querySelector(".container");
const timelineWidth = timeline.offsetWidth;                                  // ≈ viewport width (100vw)
const scrollerWidth = scroller.offsetWidth;                                  // width of the [Drag] pill
const gap = parseInt(window.getComputedStyle(document.body).fontSize);       // root font-size in px (≈16)

const maxDragX = timelineWidth - scrollerWidth - 2 * gap;                    // usable travel of the handle
```
`gap` (= the computed body font-size, ~16px) is reused as the left/right inset of the handle's travel.

### 2. Inject the 50 ruler ticks
Before creating the Draggable, append **50** `.marker` divs into `.timeline`:
```js
for (let i = 0; i < 50; i++) {
  const marker = document.createElement("div");
  marker.classList.add("marker");
  timeline.appendChild(marker);
}
```
They become flex children spread by `justify-content:space-around`, forming the tick ruler behind/around the scrubber.

### 3. The Draggable scrubber → track pan
```js
Draggable.create(scroller, {
  type: "x",
  bounds: {
    minX: gap,
    maxX: timelineWidth - scrollerWidth - gap,
  },
  onDrag: function () {
    let progress   = (this.x - gap) / maxDragX;                 // normalize handle x → 0..1
    let containerX = -400 * (timelineWidth / 100) * progress;   // 0..1 → 0 .. -400vw
    gsap.to(container, {
      x: containerX,
      duration: 1,
      ease: "power3.out",
    });
  },
});
```

Precise behavior:
- **`type: "x"`** — the handle only slides horizontally.
- **`bounds`** clamp the handle between `minX = gap` and `maxX = timelineWidth − scrollerWidth − gap`, i.e. it can never leave the ruler (a one-`gap` inset on each side). The width of that travel equals `maxDragX`.
- **`onDrag`** fires every frame the handle moves. `this.x` is the handle's live x-offset in px. `progress = (this.x − gap) / maxDragX` converts it to a **0→1** ratio across the rail.
- The pan target is `containerX = −400 · (timelineWidth/100) · progress`. Since `timelineWidth/100` is one `vw` in px, this is exactly **−400vw · progress** → at `progress = 0` the track sits at `x = 0` (section 1 visible), at `progress = 1` the track is at **−400vw**, revealing the 5th (last) 100vw section. The 500vw track minus the 100vw viewport = 400vw of travel — the mapping consumes it exactly.
- **The momentum/trailing feel**: each `onDrag` re-issues `gsap.to(container, { x: containerX, duration: 1, ease: "power3.out" })`. GSAP overwrites the in-flight tween on every frame, so the track is continuously chasing a moving 1-second `power3.out` target. The result is a soft, inertial lag — the track glides toward and eases to a stop behind the handle instead of tracking it rigidly, and keeps easing for up to ~1s after you release.

No ScrollTrigger, no scrub, no pin, no timeline sequencing, no other tweens.

## Assets / images
**9 images**, a cohesive **moody editorial fashion series** on a black page, each filling its flex slot at **90vh tall** with `object-fit:cover` (so source aspect is forgiving; the wide `flex:2` slots read landscape-ish, the narrow `flex:1` slots read portrait-ish). Mix warm golden-hour outdoor fashion, high-contrast black-and-white studio portraits, motion shots, and clean product still-lifes of leather accessories — all dark, elegant, dystopian-editorial. By role:
1. **img-1** (section 2, wide `flex:2`): warm golden-hour outdoor fashion shot, figures seen from behind in earth-tone outerwear in a dry grassy field.
2. **img-2** (section 2, narrow `flex:1`): clean product still-life — a hand holding a tan leather zip pouch against a plain off-white backdrop.
3. **img-3** (section 2, narrow `flex:1`): moody black-and-white studio torso in an oversized tailored blazer, diagonal shafts of light.
4. **img-4** (section 3, narrow `flex:1`): product still-life — a hand carrying a tan leather flat clutch/sleeve against a light crumpled-paper backdrop.
5. **img-5** (section 3, wide `flex:2`): editorial portrait lit by a hard shaft of light inside a derelict concrete interior, layered casualwear.
6. **img-6** (section 3, narrow `flex:1`): black-and-white low-angle portrait, sunglasses, wind blowing hair against a bright sky.
7. **img-7** (section 5, narrow `flex:1`): product still-life — a figure carrying a tan leather shoulder/hobo bag against a warm beige backdrop.
8. **img-8** (section 5, wide `flex:2`): golden-hour outdoor portrait from behind, earth-tone wool coat, weathered fence in a dry field.
9. **img-9** (section 5, narrow `flex:1`): black-and-white motion shot — a figure twirling in a flowing dark dress and platform boots.

## Behavior notes
- The **page never scrolls** — `.container` is `position:absolute` and moved only by GSAP; the sole interaction is dragging the bottom scrubber. Nav and timeline are `position:fixed` over the stage.
- **No autoplay / no loop** — the track is at rest (`x:0`, section 1) on load and only moves while (and shortly after) the handle is dragged.
- The mapping is anchored to `timelineWidth` (read on load), so it is correct at the initial viewport size; there is no `resize` re-measure — a mid-session resize would leave the pan ratio slightly off (matches the original, which sets everything once in `window.onload`).
- Desktop-oriented drag interaction; Draggable also handles touch, so a finger-drag on the pill works on touch devices.
- Reduced-motion: nothing animates on its own, so it's already calm; the only motion is user-initiated dragging.
