# Stories Slideshow — Fullscreen Instagram-Stories Carousel with Clip-Path Image Swaps & Custom Cursor

## Goal
Build a **fullscreen, single-view "Stories" slideshow** (Instagram-Stories style). One story is on screen at a time over a dimmed full-bleed background image. Each story **auto-advances every 4s**, and a segmented progress bar at the top fills linearly over that 4s. You can also **step manually by clicking**: clicking the left half of the screen goes **Prev**, the right half goes **Next**. The star effect is the **transition between stories**: the incoming background image **wipes in via an animated `clip-path`** (from the right on Next, from the left on Prev) while the outgoing image **scales up to 2× and rotates** as the incoming image **scales down from 2× and counter-rotates** into place — a zoom/rotate crossfade. Simultaneously the **profile name and the three title lines slide-swap inside clip-path masks** (old text scrolls out, new text scrolls in), and the completed progress segment **swipes away**. A **blurred glassy custom cursor** follows the pointer with lag and reads `PREV` / `NEXT` depending on which half of the screen you're on.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use **`gsap`** (npm) only — **no plugins, no ScrollTrigger, no Lenis, no SplitText**. All motion is imperative `gsap.to` / `gsap.fromTo` / `gsap.set` driven by timers and pointer events. Runs in one `script.js` with a separate `data.js` exporting the story array. No build framework beyond a Vite-style dev server that resolves the `gsap` npm import.

## Layout / HTML
A single `.container` holding: the custom cursor, the background-image layer, and the centered story content (progress indices + profile row, then the title + link). Class names are load-bearing — the JS queries them. The initial DOM is pre-populated with **story 1**.

```html
<div class="container">
  <div class="cursor"><p></p></div>

  <div class="story-img">
    <div class="img"><img src="/story-1.jpg" alt="" /></div>
  </div>

  <div class="story-content">
    <div class="row">
      <div class="indices">
        <div class="index"><div class="index-highlight"></div></div>
        <div class="index"><div class="index-highlight"></div></div>
        <div class="index"><div class="index-highlight"></div></div>
        <div class="index"><div class="index-highlight"></div></div>
        <div class="index"><div class="index-highlight"></div></div>
        <div class="index"><div class="index-highlight"></div></div>
      </div>

      <div class="profile">
        <div class="profile-icon"><img src="/profile-1.jpg" alt="" /></div>
        <div class="profile-name"><p>Palette</p></div>
      </div>
    </div>

    <div class="row">
      <div class="title">
        <div class="title-row"><h1>Showcasing creative</h1></div>
        <div class="title-row"><h1>portfolios and projects</h1></div>
        <div class="title-row"><h1>from top designers</h1></div>
      </div>
      <div class="link"><a href="#" target="_blank">Read More</a></div>
    </div>
  </div>
</div>

<script type="module" src="./script.js"></script>
```

There are **6 stories** and therefore **6 `.index` segments** (one segment per story). The number of `.title-row`s (3) is fixed — every story's title is exactly **3 lines**.

### Story data (`data.js`) — fictional demo copy, no real brands
Export `const stories = [...]` with **6** entries; each has `profileImg`, `profileName`, a **3-line** `title` array, `linkLabel`, `linkSrc`, `storyImg`. The initial DOM above must match `stories[0]`.

| # | profileName | title (3 lines) | linkLabel |
|---|---|---|---|
| 1 | Palette | "Showcasing creative" / "portfolios and projects" / "from top designers" | Read More |
| 2 | Driftwork | "Inspiring design" / "ideas and visual" / "creations from experts" | Discover |
| 3 | Laureate | "Award-winning web" / "design and development" / "projects" | Check It Out |
| 4 | Formary | "Curated design" / "inspiration for" / "creative professionals" | See More |
| 5 | Bloomfield | "The latest in" / "design trends" / "and tutorials" | Explore |
| 6 | Foundry | "Practical tips" / "for web designers" / "and developers" | Visit Site |

`linkSrc` can be `#` for all. `storyImg`/`profileImg` point at the 6 background images and 6 avatars.

## Styling
Global reset `* { margin:0; padding:0; box-sizing:border-box }`. `html, body { width:100%; height:100%; background:#000; cursor:none; font-family:"PP Neue Montreal", "Neue Montreal", "Inter", system-ui, sans-serif }` — a clean neutral grotesque; the original uses a proprietary Neue Montreal, any similar grotesque is fine. **`cursor:none`** globally hides the native cursor (we draw our own).

- `img { position:absolute; top:0; left:0; width:100%; height:100%; object-fit:cover }` — every image cover-fills its box.
- Type: `h1, p, a { color:#fff; text-decoration:none; font-weight:400 }`. `h1 { font-size:36px }`. `p, a { font-size:16px }`.
- `.container { width:100vw; height:100vh; overflow:hidden }`.

**Custom cursor** — a glassy blurred disc:
```css
.cursor {
  position:absolute; top:0; left:0; width:100px; height:100px;
  display:flex; justify-content:center; align-items:center;
  background:rgba(255,255,255,0.05); backdrop-filter:blur(10px);
  border-radius:100%; pointer-events:none; z-index:2;
}
.cursor p { font-size:12px; text-transform:uppercase; }   /* reads PREV / NEXT */
```

**Background layer** — dimmed full-bleed:
```css
.story-img { position:absolute; top:0; left:0; width:100vw; height:100vh; overflow:hidden; opacity:0.5; }
.img       { position:absolute; top:0; left:0; width:100%; height:100%; }
```
`.story-img` holds one or two `.img` children during a transition (old + new); `opacity:0.5` darkens the photo so the white text stays legible.

**Centered content column** (30% wide, vertically split top/bottom):
```css
.story-content {
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  padding:4em 0; width:30%; height:100%;
  display:flex; flex-direction:column; justify-content:space-between;
}
```

**Segmented progress bar** (thin hairlines across the top):
```css
.indices { width:100%; height:10px; display:flex; justify-content:space-between; align-items:center; gap:0.25em; }
.index   { position:relative; width:100%; height:1px; background:rgba(255,255,255,0.25); }
.index-highlight { position:absolute; top:0; left:0; width:0%; height:100%; background:#fff; transform:scaleX(100%); }
```
Each `.index` is a full-width 1px track at 25% white; its `.index-highlight` is the white fill that grows in `width`.

**Profile row & masked text windows:**
```css
.profile { width:100%; height:60px; display:flex; gap:1em; align-items:center; }
.profile-icon { position:relative; width:40px; height:40px; border-radius:100%; overflow:hidden; }
.profile-name { position:relative; width:200px; height:20px; clip-path:polygon(0 0, 100% 0, 100% 100%, 0% 100%); }
.title-row    { position:relative; width:100%; height:42px; clip-path:polygon(0 0, 100% 0, 100% 100%, 0% 100%); }
.title-row h1, .profile-name p { position:absolute; top:0; }
```
The rectangular `clip-path` on `.profile-name` (20px tall) and each `.title-row` (42px tall) acts as an **overflow mask**: the absolutely-positioned `<p>`/`<h1>` inside can slide vertically and be clipped at the window edges. This is what makes the text swap read as a rolling reveal.

**"Read More" link** with a 1px underline:
```css
.link { position:relative; width:max-content; margin:2em 0; padding:0.25em 0; }
.link::after { content:""; position:absolute; top:100%; left:0; width:100%; height:1px; background:#fff; }
```

**Palette:** pure black `#000` ground, pure white `#fff` text/fills, backgrounds dimmed to 0.5 opacity. No accent colors.

## GSAP effect (be exact)

### Module state & constants
```js
import gsap from "gsap";
import { stories } from "./data.js";

let activeStory = 0;
const storyDuration = 4000;       // ms per story (progress fill + auto-advance)
const contentUpdateDelay = 0.4;   // s — delay applied to every text slide tween
let direction = "next";           // updated by pointer half; drives manual clicks
let storyTimeout;                 // the auto-advance timer

const cursor = document.querySelector(".cursor");
const cursorText = cursor.querySelector("p");
```

### 1. Progress-segment fill — `animateIndexHighlight(index)`
Resets the segment then fills it linearly over the full story duration:
```js
gsap.set(highlight, { width: "0%", scaleX: 1, transformOrigin: "right center" });
gsap.to(highlight, { width: "100%", duration: storyDuration / 1000 /* 4 */, ease: "none" });
```
So the active story's white bar grows `width 0% → 100%` over **4s, linear**. When it completes, the auto-advance fires.

### 2. Retire a finished/left segment — `resetIndexHighlight(index, dir)`
Kills any running tween on that highlight, then:
```js
gsap.killTweensOf(highlight);
gsap.to(highlight, {
  width: dir === "next" ? "100%" : "0%",
  duration: 0.3,
  onStart: () => {
    gsap.to(highlight, { transformOrigin: "right center", scaleX: 0, duration: 0.3 });
  },
});
```
On **Next**, the segment snaps its `width` to `100%` (complete) while simultaneously `scaleX → 0` from `transformOrigin:right center` — it fills then **swipes out to the right** over 0.3s. On **Prev**, `width → 0%` while `scaleX → 0`. Both tweens are 0.3s (default ease).

### 3. Incoming image clip-path wipe — `animateNewImage(imgContainer, dir)`
The new `.img` starts collapsed to one edge and expands to a full rectangle:
```js
gsap.set(imgContainer, {
  clipPath: dir === "next"
    ? "polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)"   // collapsed to right edge
    : "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)",          // collapsed to left edge
});
gsap.to(imgContainer, {
  clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", // full rect
  duration: 1, ease: "power4.inOut",
});
```
So the new background **wipes in from the right (Next) or left (Prev)** over **1s, `power4.inOut`**.

### 4. Zoom/rotate crossfade — `animateImageScale(currentImg, upcomingImg, dir)`
The outgoing image blows up and rotates away; the incoming image shrinks in from an over-scaled counter-rotation:
```js
gsap.fromTo(currentImg,
  { scale: 1, rotate: 0 },
  { scale: 2, rotate: dir === "next" ? -25 : 25, duration: 1, ease: "power4.inOut",
    onComplete: () => currentImg.parentElement.remove() }   // remove old .img after
);
gsap.fromTo(upcomingImg,
  { scale: 2, rotate: dir === "next" ? 25 : -25 },
  { scale: 1, rotate: 0, duration: 1, ease: "power4.inOut" }
);
```
Both are **1s, `power4.inOut`**. Outgoing: `scale 1→2`, `rotate 0→∓25°`. Incoming: `scale 2→1`, `rotate ±25°→0°`. The old `.img` container is removed on complete so only the settled image remains.

### 5. Masked text swap (profile name + 3 title lines)
This spans two phases inside `changeStory(...)`, `t` measured from the call:

**(a) Slide the OLD text out — fired immediately, delayed 0.4s:**
```js
gsap.to(".profile-name p", { y: dir === "next" ? -24 : 24, duration: 0.5, delay: contentUpdateDelay });
gsap.to(".title-row h1",   { y: dir === "next" ? -48 : 48, duration: 0.5, delay: contentUpdateDelay });
```
On Next the current profile `<p>` slides **up** `-24px` and each title `<h1>` slides **up** `-48px` (out the top of their masks); on Prev they slide **down** `+24/+48px`. (Slide distances exceed the 20px / 42px mask heights so the text fully clears.) Runs `t=0.4s → 0.9s`, duration 0.5s.

**(b) After a `setTimeout(..., 200)`**, build & slide the NEW text in. Append a fresh `<p>` to `.profile-name` and fresh `<h1>`s to each `.title-row`, pre-offset off-window, then tween to `y:0`:
```js
newProfileName.style.transform = dir === "next" ? "translateY(24px)" : "translateY(-24px)";
gsap.to(newProfileName, { y: 0, duration: 0.5, delay: contentUpdateDelay });

// for each of the 3 title lines:
newTitle.style.transform = dir === "next" ? "translateY(48px)" : "translateY(-48px)";
gsap.to(newTitle, { y: 0, duration: 0.5, delay: contentUpdateDelay });
```
On Next the new text starts **below** its mask (`+24 / +48`) and rises to `0`; on Prev it starts **above** (`-24 / -48`) and drops to `0`. Because the tween is created inside the 200ms timeout with its own `delay:0.4`, the new text arrives at `t≈0.6s → 1.1s` — overlapping the old text's exit for a continuous rolling swap.

**(c) `cleanUpElements()`** keeps at most **2** children in `.profile-name` and in each `.title-row` (removes the oldest `firstChild` when count > 2) so stale DOM doesn't pile up across transitions.

### 6. `changeStory(isAutomatic = true)` — the orchestrator
```js
const previousStory = activeStory;
const currentDirection = isAutomatic ? "next" : direction;   // auto is always Next
activeStory = currentDirection === "next"
  ? (activeStory + 1) % stories.length
  : (activeStory - 1 + stories.length) % stories.length;      // wraps both ways
const story = stories[activeStory];
```
Then, in order:
1. Fire the **old-text-out** tweens (§5a).
2. Capture `currentImgContainer = ".story-img .img"` and its `<img>`.
3. **`setTimeout(200ms)`:** append new text (§5b) and tween in; create a new `.img` div + `<img src=story.storyImg>` appended to `.story-img`; call `animateNewImage(newImgContainer, dir)` (§3) and `animateImageScale(currentImg, newImg, dir)` (§4); `resetIndexHighlight(previousStory, dir)` (§2) and `animateIndexHighlight(activeStory)` (§1); `cleanUpElements()` (§5c); then `clearTimeout(storyTimeout)` and schedule the next auto-advance: `storyTimeout = setTimeout(() => changeStory(true), storyDuration)`.
4. **`setTimeout(600ms)`:** swap the `.profile-icon img` `src` to `story.profileImg`, and set `.link a` text to `story.linkLabel` + `href` to `story.linkSrc`.

### 7. Custom cursor follow + direction (pointer move)
```js
document.addEventListener("mousemove", ({ clientX, clientY }) => {
  gsap.to(cursor, {
    x: clientX - cursor.offsetWidth / 2,   // center the 100px disc on the pointer
    y: clientY - cursor.offsetHeight / 2,
    ease: "power2.out", duration: 0.3,     // lagged follow
  });
  if (clientX < window.innerWidth / 2) { cursorText.textContent = "Prev"; direction = "prev"; }
  else                                 { cursorText.textContent = "Next"; direction = "next"; }
});
```
The disc trails the pointer with a 0.3s `power2.out` ease and its label flips **PREV** (left half) / **NEXT** (right half), which also sets the click direction.

### 8. Click to step
```js
document.addEventListener("click", () => {
  clearTimeout(storyTimeout);
  resetIndexHighlight(activeStory, direction);   // retire the current segment in the click direction
  changeStory(false);                            // step manually using `direction`
});
```

### 9. Init (on load)
```js
storyTimeout = setTimeout(() => changeStory(true), storyDuration);  // first auto-advance in 4s
animateIndexHighlight(activeStory);                                 // start filling segment 0
```

### Per-transition timeline summary (t in seconds from `changeStory`)
- `t=0.2 → 1.2` — background clip-path wipe (§3) + zoom/rotate crossfade (§4), `power4.inOut`; old `.img` removed at 1.2.
- `t=0.2 → 0.5` — previous progress segment fills-and-swipes-away (§2).
- `t=0.2 → 4.2` — new progress segment fills linearly (§1); auto-advance fires at ~4.2.
- `t=0.4 → 0.9` — old profile/title text slides out (§5a).
- `t≈0.6 → 1.1` — new profile/title text slides in (§5b).
- `t=0.6` — profile avatar + link label/href swap (step 6.4).

## Assets / images
- **6 full-bleed background images** (`story-1…6`) — moody, editorial/creative photography (studio scenes, design workspaces, abstract textures, portraits). Any aspect ratio — each is `object-fit:cover` into the 100vw×100vh box and shown at **0.5 opacity** over black, so mid-tones/dark grounds keep the white text legible. No logos or brand text baked in.
- **6 circular profile avatars** (`profile-1…6`) — small (~40px) square images cropped to a circle: simple abstract emblems / monogram marks / solid-color badges. Generic, no real brands. Order them to match the 6 story entries.

If you have fewer than 6 of either, repeat in order — the effect is identical regardless of content.

## Behavior notes
- **Auto-advance:** every story lives 4s; the progress fill and the `setTimeout` share `storyDuration`, so the bar completing coincides with the next slide. Auto-advance is always **Next**; only clicks can go **Prev**.
- **Manual step:** a click reads the last pointer half (`direction`) — left half → Prev, right half → Next — cancels the pending auto-advance, and immediately transitions. Indices wrap in both directions (story 6 → story 1 on Next, story 1 → story 6 on Prev).
- **Custom cursor:** the native cursor is hidden (`cursor:none`); the blurred glass disc with PREV/NEXT text is the only pointer. It never intercepts clicks (`pointer-events:none`).
- **Responsive** (`max-width:900px`): restore the native cursor (`html, body { cursor:default }`), **hide** `.cursor` (`display:none`), and widen the content column to full width with padding: `.story-content { width:100%; padding:2em }`. (No dedicated tap/swipe handling — clicks still work.)
- **No scroll, no pin, no plugins** — a single fixed viewport driven entirely by timers + pointer events + core GSAP tweens. Light runtime cost.
