# Mouse Image Trail — cursor spawns image tiles that drop-and-fade on pause

## Goal
Build a full-viewport interactive canvas where **moving the mouse spawns image tiles at the cursor**, leaving a trail of stacked photos across the screen. The tiles keep piling up while the pointer is moving; **the instant the pointer pauses (~100 ms of no movement) GSAP animates the entire batch straight down off-screen** with a staggered scale-down + fade, then removes them. A centered uppercase headline sits behind the trail. The star effect is the `gsap.to(".item", …)` staggered drop (`y: 1000`, `scale: 0.5`, `opacity: 0`) fired on pointer-idle.

## Tech
Vanilla HTML/CSS/JS with ES module imports, in a fresh Vite + npm project. Install and import from npm:
- **`gsap`** (3.x) — the only dependency.

```js
import gsap from "gsap";
```
**No GSAP plugins** (no ScrollTrigger, SplitText, CustomEase), no Lenis, no Three.js, no canvas/WebGL. All logic runs inside a single `DOMContentLoaded` listener. Ship exactly three files: `index.html`, `styles.css`, `script.js`.

## Layout / HTML
A minimal document: one empty `.items` layer (JS injects tiles into it) and one headline `<h1>`. Class names are load-bearing — the JS queries `.items` and animates `.item`; the CSS positions both.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Mouse Image Trail</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div class="items"></div>
    <h1>
      Move your mouse <br />
      to explore
    </h1>
    <script type="module" src="./script.js"></script>
  </body>
</html>
```

- `.items` starts **empty** — every tile is created in JS at runtime.
- The `<h1>` copy is `Move your mouse` / `to explore` with a `<br>` between the two lines. Neutral demo text, no brands.

## Styling
```css
* { margin: 0; padding: 0; box-sizing: border-box; }

html, body { background-color: orange; }   /* CSS keyword orange = #ffa500 */

h1 {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  font-family: "PP Neue Montreal";
  font-size: 40px;
  font-weight: 400;
  text-transform: uppercase;
  line-height: 100%;
  z-index: 1;
}

.items {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: 2;      /* the trail layer sits ABOVE the headline */
}

.item {
  position: absolute;
  width: 150px;
  height: 200px;
  background: #000;   /* black backing shows behind the image while it loads */
  overflow: hidden;   /* clips the cover-cropped image to the tile box */
}

.item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
```

Details that matter:
- **Background** is the CSS keyword `orange` (`#ffa500`) on both `html` and `body`.
- **Font**: `"PP Neue Montreal"` — a modern grotesque. It is not a public web font and will silently fall back; keep it as the first name but a stack like `"PP Neue Montreal", "Helvetica Neue", Arial, sans-serif` is fine. What matters visually: 40px, weight 400, uppercase, tight `line-height: 100%`, centered.
- **Stacking**: `.items` (`z-index: 2`) renders **over** the `<h1>` (`z-index: 1`), so spawned tiles cover the headline.
- **Tile box**: every `.item` is a fixed **150 × 200 px** (3:4 portrait) black rectangle with `overflow: hidden`; the `<img>` fills it with `object-fit: cover`.

## GSAP effect (the important part — be exhaustive)
Everything is **pointer-driven** (`mousemove`) — there is no load, scroll, hover-enter or click animation. Wrap all of it in `document.addEventListener("DOMContentLoaded", …)`.

### State (module-scope inside the DOMContentLoaded callback)
```js
const container = document.querySelector(".items");
let imageIndex = 1;            // 1-based, cycles 1..15
let animationTimeout = null;   // the pending "idle" timer
let currentlyAnimating = false;
```

### 1. Spawning a tile — `addNewItem(x, y)`
On each pointer move, create and position a fresh tile at the cursor:
```js
function addNewItem(x, y) {
  const newItem = document.createElement("div");
  newItem.className = "item";
  newItem.style.left = `${x - 75}px`;   // 150px wide → -75 centers HORIZONTALLY on cursor
  newItem.style.top  = `${y - 75}px`;   // NOT vertically centered (tile is 200px tall)

  const img = document.createElement("img");
  img.src = `img${imageIndex}.jpg`;     // cycles through img1.jpg … img15.jpg
  newItem.appendChild(img);
  imageIndex = (imageIndex % 15) + 1;   // 1,2,…,15,1,2,… (wraps after 15)

  container.appendChild(newItem);
  manageItemLimit();
}
```
- **Positioning**: `left = x - 75` centers the 150px-wide tile horizontally under the cursor. `top = y - 75` uses the *same* 75px offset even though the tile is 200px tall, so the cursor sits **75px below the tile's top edge** (125px above its bottom) — the tile hangs mostly *below/around* the pointer, intentionally not vertically centered. Use `event.pageX` / `event.pageY` for `x` / `y`.
- **Image cycling**: `imageIndex` runs 1→15 and wraps via `(imageIndex % 15) + 1`; each new tile takes the next image in sequence, so a fast sweep lays down all 15 in order before repeating.

### 2. Capping the trail — `manageItemLimit()`
```js
function manageItemLimit() {
  while (container.children.length > 20) {
    container.removeChild(container.firstChild);   // drop the OLDEST tile
  }
}
```
At most **20 tiles** exist at once; while the pointer keeps moving, the oldest tiles are silently removed from the front so the trail's length stays bounded.

### 3. The drop — `startAnimation()` (the signature GSAP tween)
```js
function startAnimation() {
  if (currentlyAnimating || container.children.length === 0) return;
  currentlyAnimating = true;
  gsap.to(".item", {
    y: 1000,
    scale: 0.5,
    opacity: 0,
    duration: 0.5,
    stagger: 0.025,
    onComplete: function () {
      this.targets().forEach((item) => {
        if (item.parentNode) item.parentNode.removeChild(item);
      });
      currentlyAnimating = false;
    },
  });
}
```
Exact spec:
- **Targets**: the selector `".item"` — GSAP resolves it to **all tiles present at call time**, animating the whole current batch together.
- **Properties animated (from current → to)**: `y` `0 → 1000` (translate 1000px straight down, well off-screen), `scale` `1 → 0.5` (shrink to half), `opacity` `1 → 0` (fade out). All three run concurrently on each tile.
- **`duration: 0.5`** seconds per tile.
- **`stagger: 0.025`** — each tile starts 25 ms after the previous one, in DOM order (oldest first), so the batch cascades downward rather than dropping in unison.
- **Ease**: none is passed, so GSAP's **default `power1.out`** applies.
- **`onComplete`**: iterates `this.targets()` and removes each animated tile from the DOM, then clears `currentlyAnimating = false`. (Tiles spawned *after* this tween was created are not part of `this.targets()` and survive to the next drop.)
- **Guards**: returns early if a drop is already running (`currentlyAnimating`) or there is nothing to animate (`container.children.length === 0`), so overlapping/empty tweens never start.

### 4. The trigger — `mousemove` on the container
```js
container.addEventListener("mousemove", function (event) {
  clearTimeout(animationTimeout);
  addNewItem(event.pageX, event.pageY);
  animationTimeout = setTimeout(startAnimation, 100);
});
```
This is the whole interaction loop, and the timing is the effect:
- The listener is on **`.items`** (the fixed full-viewport layer), not `document`/`window`.
- Every move **(a)** clears the previously-scheduled idle timer, **(b)** spawns a tile at the cursor, **(c)** re-arms a **100 ms** `setTimeout(startAnimation, 100)`.
- **While the pointer keeps moving** (events < 100 ms apart), the timer is cleared and reset on every event, so `startAnimation` never fires — tiles just accumulate (capped at 20) and trail the cursor.
- **The moment the pointer pauses** for ≥ 100 ms, the pending timer finally fires → `startAnimation()` drops the whole batch. So the trail lives while you move and evaporates the instant you stop.

## Assets / images
**15 portrait images** referenced as `img1.jpg … img15.jpg`, each shown inside a **150 × 200 px (3:4) tile** with `object-fit: cover` (framing is forgiving). Cinematic / experimental / editorial mood, coherent as a set. Roles by index:
1. Minimalist lone figure on a reflective wet surface with a diagonal shaft of light, teal/cyan tones.
2. Close-up of a golden reflective astronaut helmet against a dark green backdrop.
3. Lone astronaut silhouette walking through foggy golden desert, large glowing sun behind.
4. Dark moody portrait of a woman, hands raised to her face in low light with jewelry.
5. Silhouette pressing a hand against frosted glass, warm orange backlight.
6. Moody studio portrait of a blonde woman in black against a dark background.
7. Figure in a wide decorative conical hat and futuristic sunglasses, teal/lavender tones.
8. Small golden lucky-cat figurine inside a red circle on plain white.
9. Person in a black suit, head obscured by white feathers/fabric, grey background.
10. Golden-hour city skyline silhouette under a warm orange sky.
11. Soft-lit figure with wet hair leaning to one side, pale blue tones.
12. Green-lit muscular figure sculpture amid soft bokeh light spots.
13. Still life of pink roses and green leaves in an S-shape on black.
14. Portrait of a woman with sleek dark hair in large futuristic visor sunglasses, warm brown backdrop.
15. Woman sitting on a paddleboard on calm water at dusk.

No brands or logos. If fewer than 15 real images are available, any consistent set of portrait-oriented photos works; the effect does not depend on the specific subjects.

## Behavior notes
- **Desktop / mouse only** — the whole thing is `mousemove`-driven with no touch fallback; leave it as-is (pointer-first).
- **No reduced-motion guard** in the original.
- The trail self-limits to 20 live tiles; each pause fires exactly one staggered drop that cleans itself up in `onComplete`.
- Tiles render **above** the centered headline (`z-index` 2 vs 1), so a busy trail temporarily hides the "Move your mouse to explore" text.

## Acceptance check (reproduction is faithful only if all hold)
- Full-orange (`#ffa500`) viewport with a centered, uppercase, 40px "Move your mouse / to explore" headline visible at rest (empty `.items`).
- Moving the mouse spawns 150×200 black-backed image tiles at the cursor (horizontally centered on it), cycling through 15 images and trailing the pointer, capped at ~20 on screen.
- Pausing the pointer ~100 ms triggers one `gsap.to(".item", …)` that sends the whole batch **down (`y: 1000`) while scaling to 0.5 and fading to 0**, staggered 0.025s, 0.5s each, default `power1.out` ease, then removes the tiles.
- Console is clean (zero errors); only `gsap` is imported, no plugins.
