---
name: qdhenry/gsap-animation
source: https://app.decimal.ai/s/qdhenry-gsap-animation@1/SKILL.md
source_sha256: 685d7cec3e61
---

## Table of Contents

1. [Installation & TypeScript Setup](#installation--typescript-setup)
2. [Core Concepts](#core-concepts)
3. [Tweens](#tweens)
4. [Timelines](#timelines)
5. [Easing](#easing)
6. [Staggers](#staggers)
7. [Control Methods](#control-methods)
8. [Utility Methods](#utility-methods)
9. [Context & Cleanup](#context--cleanup)
10. [Responsive Animations — matchMedia()](#responsive-animations--matchmedia)
11. [Plugins Overview](#plugins-overview)
12. [ScrollTrigger](#scrolltrigger)
13. [ScrollSmoother](#scrollsmoother)
14. [Flip Plugin](#flip-plugin)
15. [SplitText Plugin](#splittext-plugin)
16. [React Integration — useGSAP()](#react-integration--usegsap)
17. [Performance Tips & Best Practices](#performance-tips--best-practices)
18. [Helper Functions](#helper-functions)

---

## Installation & TypeScript Setup

```bash
npm install gsap
```

TypeScript definitions are bundled with the package. If you need to point your compiler to them explicitly:

```json
// tsconfig.json
{
  "compilerOptions": { ... },
  "files": [
    "node_modules/gsap/types/index.d.ts"
  ]
}
```

**Basic import:**

```ts
import { gsap } from "gsap";
```

**Importing plugins:**

```ts
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Flip } from "gsap/Flip";
import { SplitText } from "gsap/SplitText";
import { ScrollSmoother } from "gsap/ScrollSmoother";
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";

// Register all plugins once, before use
gsap.registerPlugin(ScrollTrigger, Flip, SplitText, ScrollSmoother);
```

**Recommended: single `gsap.ts` barrel file** to avoid duplicate registrations in large projects:

```ts
// gsap.ts
export * from "gsap";
export * from "gsap/ScrollTrigger";
export * from "gsap/Flip";

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Flip } from "gsap/Flip";

gsap.registerPlugin(ScrollTrigger, Flip);
```

Then in other files:

```ts
import { gsap, ScrollTrigger } from "../gsap";
```

**UMD/dist format** (for older build tools that don't support ES modules):

```ts
import { gsap } from "gsap/dist/gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
```

> **Tree shaking:** Always call `gsap.registerPlugin(...)` to prevent build tools from dropping plugins during tree shaking. It's safe to register the same plugin multiple times.

---

## Core Concepts

GSAP has two primary animation primitives:

**Tween** — animates properties on target(s). Created with `gsap.to()`, `gsap.from()`, or `gsap.fromTo()`.

**Timeline** — a container for sequencing multiple tweens and other timelines. Created with `gsap.timeline()`.

Both extend an `Animation` base class and share the same control methods (`play`, `pause`, `reverse`, `seek`, `timeScale`, etc.).

GSAP can animate **any numeric property** of any JavaScript object — not just CSS. DOM elements, canvas contexts, WebGL uniforms, plain objects — anything works.

**Transform shorthand** — GSAP provides shorthand properties that map to CSS transforms:

| GSAP property               | CSS equivalent                   |
| --------------------------- | -------------------------------- |
| `x`, `y`                    | `translateX`, `translateY`       |
| `xPercent`, `yPercent`      | `translateX(%)`, `translateY(%)` |
| `rotation`                  | `rotate` (degrees)               |
| `rotationX`, `rotationY`    | `rotateX`, `rotateY`             |
| `scale`, `scaleX`, `scaleY` | `scale`                          |
| `skewX`, `skewY`            | `skew`                           |

---

## Tweens

```ts
// Animate TO values
gsap.to(".selector", {
  x: 100,
  y: 50,
  rotation: 360,
  backgroundColor: "red", // camelCase CSS
  duration: 1, // seconds (default: 0.5)
  delay: 0.5,
  ease: "power2.inOut",
  stagger: 0.1, // offset start per target
  paused: false,
  overwrite: "auto", // "auto" | true | false
  repeat: 2, // -1 = infinite
  repeatDelay: 1,
  repeatRefresh: true, // re-evaluate dynamic values each repeat
  yoyo: true, // A→B→A ping-pong
  yoyoEase: "power1.in", // separate ease for reverse
  immediateRender: false,
  onStart: () => {},
  onUpdate: () => {},
  onComplete: () => {},
  onRepeat: () => {},
  onReverseComplete: () => {},
});

// Animate FROM values (immediateRender: true by default)
gsap.from(".selector", { x: -200, opacity: 0, duration: 1 });

// Animate from → to (define both explicitly)
gsap.fromTo(
  ".selector",
  { x: -200, opacity: 0 },
  { x: 0, opacity: 1, duration: 1 },
);

// Set immediately (no animation)
gsap.set(".selector", { x: 100, opacity: 0 });
```

**Function-based values** — called once per target, returning the value to use:

```ts
gsap.to(".box", {
  x: (index, target, targets) => index * 100,
  duration: 1,
});
```

**Random values:**

```ts
gsap.to(".box", {
  x: "random(-100, 100)", // random number in range
  x: "random(-100, 100, 5)", // rounded to nearest 5
  x: "random([0, 100, 200])", // random from array
});
```

**Relative values:**

```ts
gsap.to(".box", { x: "+=50", rotation: "-=30" });
```

**Keyframes:**

```ts
gsap.to(".box", {
  keyframes: [
    { x: 100, duration: 1 },
    { y: 50, duration: 0.5 },
    { opacity: 0, duration: 0.5 },
  ],
});
```

**Special properties:**

| Property            | Description                                 |
| ------------------- | ------------------------------------------- |
| `duration`          | Duration in seconds (default `0.5`)         |
| `delay`             | Delay before start (seconds)                |
| `ease`              | Easing function name or function            |
| `stagger`           | Start time offset per target                |
| `repeat`            | Number of repeats (`-1` = infinite)         |
| `repeatDelay`       | Pause between repeats                       |
| `yoyo`              | Ping-pong direction on repeat               |
| `paused`            | Start paused                                |
| `overwrite`         | Kill conflicting tweens: `"auto"` or `true` |
| `immediateRender`   | Render first frame immediately              |
| `onComplete`        | Callback on finish                          |
| `onStart`           | Callback on start                           |
| `onUpdate`          | Callback every tick                         |
| `onRepeat`          | Callback on each repeat                     |
| `onReverseComplete` | Callback when reversed to start             |
| `id`                | Unique string ID for `gsap.getById()`       |
| `data`              | Arbitrary data attached to the tween        |
| `callbackScope`     | `this` scope for callbacks                  |
| `startAt`           | Define starting values for any property     |
| `keyframes`         | Array of tween vars for sequential states   |

---

## Timelines

```ts
const tl = gsap.timeline({
  delay: 0.5,
  paused: false,
  repeat: -1,
  repeatDelay: 1,
  yoyo: true,
  defaults: {
    // all child tweens inherit these
    duration: 1,
    ease: "power2.out",
  },
  onComplete: () => {},
});

// Chain tweens (sequential by default)
tl.to(".a", { x: 100 }).to(".b", { y: 200 }).to(".c", { rotation: 360 });
```

**Position parameter** — controls where each tween is placed in the timeline:

```ts
tl.to(".a", { x: 100 }, 0.5); // absolute 0.5s from start
tl.to(".b", { x: 100 }, "+=0.3"); // 0.3s after previous ends
tl.to(".c", { x: 100 }, "-=0.2"); // overlap 0.2s with previous
tl.to(".d", { x: 100 }, "myLabel"); // at label position
tl.to(".e", { x: 100 }, "myLabel+=0.5"); // 0.5s after label
tl.to(".f", { x: 100 }, "<"); // same start as previous tween
tl.to(".g", { x: 100 }, "<0.2"); // 0.2s after start of previous
tl.to(".h", { x: 100 }, "-=50%"); // overlap half of new tween's duration
```

**Labels:**

```ts
tl.addLabel("intro", 2); // add label at 2s
tl.seek("intro"); // jump playhead to label
```

**Nesting timelines:**

```ts
function buildScene1() {
  const tl = gsap.timeline();
  tl.to(".a", { x: 100 }).to(".b", { y: 50 });
  return tl;
}

const master = gsap.timeline();
master.add(buildScene1()).add(buildScene2(), "-=0.5"); // overlap slightly
```

---

## Easing

```ts
ease: "none"; // linear
ease: "power1.out"; // default
ease: "power1.in";
ease: "power1.inOut";
// power1 through power4, circ, expo, sine — all have .in .out .inOut

ease: "elastic"; // springy
ease: "elastic.out(1, 0.3)"; // amplitude, period
ease: "back"; // overshoots slightly
ease: "back.out(1.7)"; // configurable overshoot
ease: "bounce"; // bouncy landing
ease: "steps(12)"; // stepped/frame-by-frame

// EasePack (import separately)
ease: "rough({ strength: 1, points: 20, randomize: true })";
ease: "slow(0.7, 0.7, false)";
ease: "expoScale(1, 2)";

// Custom (requires CustomEase plugin)
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(CustomEase);
CustomEase.create("myEase", "0.23, 1, 0.32, 1");
ease: "myEase";
```

**Set global defaults:**

```ts
gsap.defaults({ ease: "power2.inOut", duration: 0.8 });
```

---

## Staggers

```ts
// Simple — seconds between each start
gsap.to(".box", { y: 100, stagger: 0.1 });

// Advanced stagger object
gsap.to(".box", {
  y: 100,
  stagger: {
    each: 0.1, // delay between each (preferred over `amount`)
    amount: 1, // total time split across all targets
    from: "center", // "start" | "end" | "center" | "edges" | "random" | index
    grid: "auto", // [rows, cols] or "auto"
    axis: "x", // "x" | "y" — for grid-based staggers
    ease: "power2.inOut",
    repeat: -1, // repeat per sub-tween
    yoyo: true,
  },
});

// Function-based stagger (return total delay from start)
gsap.to(".box", {
  y: 100,
  stagger: (index, target, list) => index * 0.08,
});
```

---

## Control Methods

```ts
const anim = gsap.to(".box", { x: 100, duration: 2, paused: true });

// Playback
anim.play();
anim.pause();
anim.resume(); // respects current direction
anim.reverse();
anim.restart();
anim.timeScale(2); // 2 = double speed, 0.5 = half
anim.seek(1.5); // jump to time (seconds) or label
anim.progress(0.5); // jump to 50%
anim.totalProgress(0.75); // includes repeats

// State
anim.isActive(); // true if currently animating
anim.paused(); // getter
anim.paused(true); // setter
anim.reversed();
anim.duration();
anim.totalDuration(); // includes repeats
anim.time();
anim.totalTime();

// Destruction
anim.kill(); // stop & remove from parent
anim.revert(); // stop & restore to pre-animation state
anim.invalidate(); // flush recorded start/end values

// Promise
await anim.then(() => console.log("done"));

// Callbacks
anim.eventCallback("onComplete", () => {});

// Timeline-specific
tl.add(thing, position);
tl.call(fn, params, position);
tl.getChildren();
tl.clear();
tl.tweenTo("label", { duration: 0.5 });
tl.tweenFromTo("start", "end", { duration: 1 });
```

**Global kill helpers:**

```ts
gsap.killTweensOf(".selector");
gsap.killTweensOf(myElement);
gsap.killTweensOf(myFunction); // kills delayedCalls
```

**quickSetter & quickTo:**

```ts
// quickSetter: fastest way to repeatedly SET a property (no animation)
const setX = gsap.quickSetter("#id", "x", "px");
document.addEventListener("mousemove", (e) => setX(e.clientX));

// quickTo: same but ANIMATES to the new value each call
const xTo = gsap.quickTo("#id", "x", { duration: 0.4, ease: "power3" });
document.addEventListener("mousemove", (e) => xTo(e.pageX));
```

**Global ticker:**

```ts
gsap.ticker.add((time, deltaTime, frame) => {
  // runs every GSAP tick
});
gsap.ticker.remove(myFunction);
gsap.ticker.fps(60); // cap frame rate
```

---

## Utility Methods

Accessible via `gsap.utils.*`. Many return reusable functions when called without a value argument.

```ts
gsap.utils.clamp(0, 100, 150); // → 100
gsap.utils.clamp(0, 100); // → (v) => clamp(v) — reusable fn

gsap.utils.mapRange(-10, 10, 0, 100, 5); // → 75
gsap.utils.normalize(100, 200, 150); // → 0.5 (maps to 0–1)

gsap.utils.interpolate("red", "blue", 0.5); // → "rgba(128,0,128,1)"
gsap.utils.interpolate([0, 100], 0.25); // → 25

gsap.utils.snap(5, 13); // → 15 (nearest increment of 5)
gsap.utils.snap([0, 50, 100], 73); // → 50 (nearest in array)

gsap.utils.random(0, 100); // → random number
gsap.utils.random(0, 100, 5); // → rounded to nearest 5
gsap.utils.random(["a", "b", "c"]); // → random element

gsap.utils.wrap(0, 10, 12); // → 2 (wraps around range)
gsap.utils.wrapYoyo(0, 10, 12); // → 8 (ping-pong)

gsap.utils.pipe(gsap.utils.clamp(0, 100), gsap.utils.snap(5))(108); // → 100

gsap.utils.toArray(".boxes"); // → Array from selector/NodeList
gsap.utils.selector(myEl); // → scoped selector fn: sel(".box")
gsap.utils.shuffle([1, 2, 3, 4]); // in-place shuffle → [3,1,4,2]
gsap.utils.splitColor("red"); // → [255, 0, 0]
gsap.utils.getUnit("30px"); // → "px"
gsap.utils.unitize(gsap.utils.wrap(0, 100))("150px"); // → "50px"
gsap.utils.checkPrefix("transform"); // → vendor-prefixed string
gsap.utils.distribute({ amount: 1, from: "center", ease: "power2" });
```

---

## Context & Cleanup

`gsap.context()` collects all animations/ScrollTriggers created within it so they can all be reverted at once. Essential for component-based frameworks.

```ts
const ctx = gsap.context(() => {
  gsap.to(".box", { x: 100 });
  gsap.timeline().to(".item", { y: 50 });
  ScrollTrigger.create({ ... });
  // all of the above are tracked
}, myContainerElement); // optional scope for selector text

// Later (e.g. on component unmount):
ctx.revert(); // all animations reverted, elements restored

// Add context-safe functions for event handlers
const ctx = gsap.context((self) => {
  self.add("onClick", () => {
    gsap.to(".box", { rotation: 360 }); // tracked!
  });
}, containerRef);

button.addEventListener("click", ctx.onClick);
```

---

## Responsive Animations — matchMedia()

```ts
const mm = gsap.matchMedia();

// Simple breakpoints
mm.add("(min-width: 800px)", () => {
  gsap.to(".box", { x: 500 });
  ScrollTrigger.create({ ... });

  return () => {
    // optional custom cleanup when query stops matching
  };
});

mm.add("(max-width: 799px)", () => {
  gsap.to(".box", { x: 100 });
});

// Conditions syntax (shared setup code)
mm.add(
  {
    isDesktop: "(min-width: 800px)",
    isMobile: "(max-width: 799px)",
    reduceMotion: "(prefers-reduced-motion: reduce)",
  },
  (context) => {
    const { isDesktop, reduceMotion } = context.conditions;

    gsap.to(".box", {
      rotation: isDesktop ? 360 : 180,
      duration: reduceMotion ? 0 : 1.5,
    });
  }
);

// Revert all
mm.revert();
```

---

## Plugins Overview

Register plugins before use: `gsap.registerPlugin(PluginA, PluginB)`.

| Plugin             | Purpose                                                       |
| ------------------ | ------------------------------------------------------------- |
| **ScrollTrigger**  | Scroll-based animation triggers, scrubbing, pinning           |
| **ScrollSmoother** | Native-scroll-based smooth scrolling (requires ScrollTrigger) |
| **SplitText**      | Split text into chars/words/lines for animation               |
| **Flip**           | Seamless layout/DOM-change transitions (FLIP technique)       |
| **Draggable**      | Drag & drop with physics/bounds                               |
| **Inertia**        | Momentum/glide after release (requires Draggable)             |
| **Observer**       | Unified pointer/touch/scroll event observer                   |
| **MotionPath**     | Animate along SVG or custom paths                             |
| **MorphSVG**       | Morph between SVG shapes                                      |
| **DrawSVG**        | Animate SVG stroke drawing                                    |
| **CustomEase**     | Create arbitrary cubic-bezier eases                           |
| **CustomBounce**   | Create custom bounce eases                                    |
| **CustomWiggle**   | Create custom wiggle eases                                    |
| **EasePack**       | Extra eases: `rough`, `slow`, `expoScale`                     |
| **ScrollTo**       | Animate the scroll position of any element                    |
| **TextPlugin**     | Animate text content character by character                   |
| **ScrambleText**   | Scramble/randomize text during animation                      |
| **Physics2D**      | Physics-based 2D motion                                       |
| **PhysicsProps**   | Physics-based property animation                              |
| **GSDevTools**     | Visual timeline debugger (dev only)                           |
| **PixiPlugin**     | Animate PixiJS display objects                                |
| **EaselPlugin**    | Animate EaselJS display objects                               |

---

## ScrollTrigger

```ts
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
```

**Simple trigger:**

```ts
gsap.to(".box", {
  scrollTrigger: ".box", // shorthand: trigger selector
  x: 500,
});
```

**Full config on a timeline:**

```ts
const tl = gsap.timeline({
  scrollTrigger: {
    trigger: ".container",
    start: "top top", // [trigger edge] [scroller edge]
    end: "+=500", // relative: 500px beyond start
    scrub: 1, // seconds to "catch up" (true = instant)
    pin: true, // pin the trigger element
    markers: true, // dev-only visual markers
    anticipatePin: 1, // counteract fast-scroll flash
    snap: {
      snapTo: "labels", // or number/array/fn
      duration: { min: 0.2, max: 3 },
      ease: "power1.inOut",
    },
    toggleActions: "play pause resume reset",
    // actions: play | pause | resume | reset | restart | complete | reverse | none
    // order: onEnter onLeave onEnterBack onLeaveBack
    toggleClass: "active",
    once: true, // kill after first activation
    horizontal: false,
    invalidateOnRefresh: true,
    fastScrollEnd: true,
    preventOverlaps: true,
    onEnter: (self) => {},
    onLeave: (self) => {},
    onEnterBack: (self) => {},
    onLeaveBack: (self) => {},
    onUpdate: (self) => {},
    onToggle: (self) => {},
    onRefresh: (self) => {},
    onScrubComplete: (self) => {},
  },
});
```

**Standalone ScrollTrigger:**

```ts
ScrollTrigger.create({
  trigger: "#section",
  start: "top center",
  end: "bottom center",
  onToggle: (self) => console.log("active:", self.isActive),
  onUpdate: (self) => console.log("progress:", self.progress),
});
```

**Key static methods:**

```ts
ScrollTrigger.refresh();              // recalculate all positions
ScrollTrigger.getAll();               // array of all instances
ScrollTrigger.getById("id");
ScrollTrigger.killAll();
ScrollTrigger.defaults({ markers: true }); // set defaults

// Batch — coordinate multiple triggers that fire around the same time
ScrollTrigger.batch(".card", {
  onEnter: (elements) => gsap.from(elements, { y: 50, opacity: 0, stagger: 0.1 }),
  start: "top 85%",
});

// Responsive — use matchMedia instead of ScrollTrigger.matchMedia (deprecated)
const mm = gsap.matchMedia();
mm.add("(min-width: 800px)", () => {
  ScrollTrigger.create({ ... });
});
```

**start/end position syntax:**

```
"top top"        → top of trigger meets top of viewport
"center center"  → midpoints meet
"bottom 80%"     → bottom of trigger hits 80% down viewport
"+=300"          → 300px beyond start
"top bottom-=100px" → top of trigger hits 100px above bottom of viewport
```

---

## ScrollSmoother

Requires ScrollTrigger. Adds native-scroll smooth scrolling via CSS transforms.

**Required HTML structure:**

```html
<body>
  <div id="smooth-wrapper">
    <div id="smooth-content">
      <!-- ALL content here -->
    </div>
  </div>
  <!-- position:fixed elements outside the wrapper -->
</body>
```

```ts
import { ScrollSmoother } from "gsap/ScrollSmoother";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);

// Create BEFORE any ScrollTriggers
const smoother = ScrollSmoother.create({
  smooth: 1, // seconds to catch up to native scroll
  effects: true, // enable data-speed / data-lag attributes
  smoothTouch: 0.1, // smoothing on touch devices (default: none)
  normalizeScroll: true, // prevent mobile address bar jump
  ignoreMobileResize: true,
});
```

**Parallax & lag via data attributes:**

```html
<div data-speed="0.5">half scroll speed</div>
<div data-speed="2">double scroll speed</div>
<div data-speed="auto">auto-parallax (fill parent)</div>
<div data-lag="0.5">lags 0.5s behind scroll</div>
<div data-speed="clamp(0.5)">clamped — starts at native position</div>
```

**JavaScript effects:**

```ts
smoother.effects(".box", { speed: 0.5, lag: 0.2 });
```

**Useful methods:**

```ts
ScrollSmoother.get(); // get the singleton instance
smoother.scrollTo(".section", true, "top top");
smoother.scrollTop(500);
smoother.paused(true); // halt all scrolling
smoother.smooth(2); // change smooth time on the fly
smoother.getVelocity();
smoother.kill();
```

---

## Flip Plugin

FLIP = **F**irst, **L**ast, **I**nvert, **P**lay. Animate seamlessly between two DOM states, even across layout or DOM changes.

```ts
import { Flip } from "gsap/Flip";
gsap.registerPlugin(Flip);
```

**Three-step pattern:**

```ts
// 1. Capture current state
const state = Flip.getState(".targets");

// Optionally capture extra CSS props
const state = Flip.getState(".targets", { props: "backgroundColor,color" });

// 2. Make any DOM/CSS changes
element.classList.toggle("expanded");
container.appendChild(element);

// 3. Animate from old state to new
Flip.from(state, {
  duration: 0.6,
  ease: "power1.inOut",
  absolute: true, // use position:absolute during flip
  nested: true, // handle parent+child being flipped together
  scale: false, // use width/height (default) vs scaleX/scaleY
  fade: true, // crossfade when swapping elements
  toggleClass: "flipping",
  zIndex: 100,
  onEnter: (elements) => gsap.fromTo(elements, { opacity: 0 }, { opacity: 1 }),
  onLeave: (elements) => gsap.to(elements, { opacity: 0 }),
  onComplete: () => {},
});
```

**Swapping two different elements** — give them matching `data-flip-id` attributes:

```html
<div class="card" data-flip-id="card-1"></div>
<!-- somewhere else in DOM -->
<div class="card-expanded" data-flip-id="card-1"></div>
```

**Other Flip methods:**

```ts
Flip.to(state, vars); // animate TO a saved state (reverse)
Flip.fit(target, destination); // resize/reposition target to match destination
Flip.isFlipping(element); // check if actively flipping
Flip.killFlipsOf(targets);
Flip.makeAbsolute(targets); // position:absolute while keeping visual position
```

---

## SplitText Plugin

```ts
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
```

**Basic usage:**

```ts
const split = SplitText.create(".headline", {
  type: "lines, words, chars", // what to split into
});

// Returns arrays of newly created elements
split.chars; // Array of character <div>s
split.words; // Array of word <div>s
split.lines; // Array of line <div>s

// Animate them
gsap.from(split.chars, {
  y: 60,
  opacity: 0,
  stagger: 0.03,
  duration: 0.8,
  ease: "back.out",
});

// Revert to original HTML when done
split.revert();
```

**Recommended v3.13+ pattern with autoSplit:**

```ts
SplitText.create(".headline", {
  type: "lines, words",
  mask: "lines", // wraps lines with overflow:clip for reveal effects
  autoSplit: true, // re-splits on font load or resize
  onSplit(self) {
    return gsap.from(self.lines, {
      yPercent: 100,
      opacity: 0,
      stagger: 0.1,
      duration: 0.8,
    }); // returned animation is synced on re-split
  },
});
```

**Key config options:**

| Option             | Description                                                      |
| ------------------ | ---------------------------------------------------------------- |
| `type`             | `"chars"`, `"words"`, `"lines"` (comma-separated)                |
| `mask`             | `"chars"` \| `"words"` \| `"lines"` — adds overflow clip wrapper |
| `autoSplit`        | Re-splits on font load or container resize                       |
| `onSplit`          | Callback on each split; return animation for auto-sync           |
| `linesClass`       | CSS class for line elements (`"line++"` = auto-increment)        |
| `wordsClass`       | CSS class for word elements                                      |
| `charsClass`       | CSS class for char elements                                      |
| `aria`             | `"auto"` (default) \| `"hidden"` \| `"none"`                     |
| `deepSlice`        | Handle nested elements (`<strong>`, `<a>`) spanning lines        |
| `tag`              | Wrapper tag, default `"div"` (use `"span"` for inline)           |
| `reduceWhiteSpace` | Collapse whitespace (default `true`)                             |
| `ignore`           | Selector for elements to skip splitting                          |

> **Accessibility:** By default, SplitText adds `aria-label` to the parent and `aria-hidden` to split children, so screen readers read the whole text correctly.

> **Performance tip:** Only split what you animate. Splitting thousands of nodes is expensive.

> **Custom fonts:** Use `autoSplit: true` or wrap in `document.fonts.ready.then(() => {...})` to avoid layout shift.

---

## React Integration — useGSAP()

```bash
npm install @gsap/react
```

```tsx
import { useRef } from "react";
import { gsap } from "gsap";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(useGSAP); // prevents React version discrepancies

function MyComponent() {
  const container = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      // All animations here are automatically reverted on unmount
      gsap.to(".box", { x: 360, duration: 1 });
    },
    { scope: container }, // scopes selector text to container
  );

  return (
    <div ref={container}>
      <div className="box" />
    </div>
  );
}
```

**Config options:**

```ts
useGSAP(() => { ... }, {
  dependencies: [value],  // re-run when these change (like useEffect deps)
  scope: containerRef,    // scope selector text
  revertOnUpdate: true,   // revert & re-run on every dependency change
});
```

**Event handlers (context-safe):**

Animations created inside event handlers run AFTER `useGSAP()` executes and won't be auto-cleaned up — wrap them in `contextSafe`:

```tsx
// Option 1: destructure contextSafe from return value (for handlers outside the hook)
const { contextSafe } = useGSAP({ scope: container });

const onClick = contextSafe(() => {
  gsap.to(".box", { rotation: 360 }); // safely tracked
});

return <button onClick={onClick} />;
```

```tsx
// Option 2: use 2nd argument inside the hook (for manual event listeners)
useGSAP(
  (context, contextSafe) => {
    gsap.to(".box", { x: 100 }); // safe

    const onHover = contextSafe(() => {
      gsap.to(".box", { scale: 1.2 }); // safe
    });

    boxRef.current.addEventListener("mouseenter", onHover);

    return () => {
      boxRef.current.removeEventListener("mouseenter", onHover);
    };
  },
  { scope: container },
);
```

> **React 18 Strict Mode:** GSAP effects run twice in dev. `useGSAP()` handles the cleanup correctly so animations don't double-fire. Never use plain `useEffect` for GSAP without manual cleanup.

> **SSR / Next.js:** `useGSAP()` is SSR-safe. In App Router, add `"use client"` at the top of the file.

---

## Performance Tips & Best Practices

**Prefer transform properties** — `x`, `y`, `rotation`, `scale` are GPU-composited and far cheaper than animating `top`, `left`, `width`, etc.

**Use `will-change` sparingly** — only on elements actively animating; too many defeats the purpose.

**overwrite: "auto"** — prevents conflicting tweens fighting each other without killing unrelated animations.

**Register plugins once at app root** — not inside components.

**Kill or revert animations on cleanup** — use `gsap.context()` or `useGSAP()` to avoid memory leaks.

**Use `gsap.quickSetter` for high-frequency updates** (mousemove, scroll) instead of repeated `gsap.set()` calls.

**Use `gsap.quickTo` for high-frequency animated updates** (pointer following, real-time sliders).

**Avoid animating `display` or `visibility` directly** — use `autoAlpha` (animates both `opacity` and `visibility` together).

**`autoAlpha`** — GSAP shorthand: `opacity: 0` + `visibility: hidden` when 0, `visibility: visible` when > 0.

**`clearProps`** — remove inline styles after animation:

```ts
gsap.set(".box", { clearProps: "transform" }); // remove transforms
gsap.set(".box", { clearProps: "all" }); // remove all inline styles
```

**Set defaults globally or per-timeline:**

```ts
gsap.defaults({ ease: "power2.out", duration: 0.6 });

const tl = gsap.timeline({ defaults: { ease: "back.out", duration: 0.4 } });
```

**Register effects for reuse:**

```ts
gsap.registerEffect({
  name: "fadeIn",
  effect: (targets, config) =>
    gsap.from(targets, { opacity: 0, y: 30, duration: config.duration }),
  defaults: { duration: 0.8 },
  extendTimeline: true, // tl.fadeIn(".box") works!
});

gsap.effects.fadeIn(".hero");
tl.fadeIn(".cards", { duration: 0.5 });
```

**Modifiers plugin** — dynamically transform values each frame (useful for infinite loops, clamping, etc.):

```ts
gsap.to(".box", {
  x: 1000,
  modifiers: {
    x: gsap.utils.unitize((x) => parseFloat(x) % 500), // wrap at 500px
  },
});
```

---

## Helper Functions

The GSAP team maintains an official collection of community helper functions at `gsap.com/docs/v3/HelperFunctions`. Notable ones:

| Helper                   | Purpose                                           |
| ------------------------ | ------------------------------------------------- |
| `seamlessLoop()`         | Infinite seamless carousel/ticker loop            |
| `stopOverscroll()`       | Prevent overscroll on iOS Safari                  |
| `lottieScrollTrigger()`  | Tie a Lottie animation to scroll position         |
| `blendEases()`           | Blend two eases at start/end of animation         |
| `distributeByPosition()` | Stagger irregular grid elements by position       |
| `scrubCanvasFrames()`    | Scrub through canvas image sequences on scroll    |
| `anchorProgress()`       | Progress values for SVG path anchor points        |
| `weightedRandom()`       | Biased random values using an ease curve          |
| `callAfterResize()`      | Debounced resize handler                          |
| `getScrollLookup()`      | Get element scroll position (ScrollTrigger-aware) |

---

## Quick Reference

```ts
// One-liner fade-in on scroll
gsap.from(".card", {
  scrollTrigger: { trigger: ".card", start: "top 85%" },
  opacity: 0,
  y: 40,
  duration: 0.8,
  stagger: 0.1,
});

// Text reveal with SplitText
SplitText.create("h1", {
  type: "lines",
  mask: "lines",
  autoSplit: true,
  onSplit: (self) => gsap.from(self.lines, { yPercent: 100, stagger: 0.1 }),
});

// Pinned scroll section
gsap.timeline({
  scrollTrigger: {
    trigger: ".panel",
    pin: true,
    start: "top top",
    end: "+=600",
    scrub: 1,
  },
}).to(".inner", { x: 400 }).to(".inner", { opacity: 0 });

// FLIP layout change
const state = Flip.getState(".item");
container.classList.toggle("grid-layout");
Flip.from(state, { duration: 0.6, ease: "power1.inOut", stagger:
Flip.from(state, { duration: 0.6, ease: "power1.inOut", stagger: 0.05 });
// Smooth scroll setup
ScrollSmoother.create({ smooth: 1, effects: true });
// Responsive animations
const mm = gsap.matchMedia();
mm.add("(min-width: 800px)", () => {
  gsap.to(".box", { x: 500 });
});
mm.add("(prefers-reduced-motion: reduce)", () => {
  gsap.globalTimeline.timeScale(0); // kill all motion
});
```