# Implementation Reference — All 6 Animation Layers

Complete, production-ready code for each layer of the Sebanim8 animation system. Copy and adapt to the project's structure.

---

## Table of Contents

1. [Layer 1: Smooth Scroll (Lenis)](#layer-1-smooth-scroll)
2. [Layer 2: Section Reveals (Curtain)](#layer-2-section-reveals)
3. [Layer 3: Entry Animations (data-anim)](#layer-3-entry-animations)
4. [Layer 4: Parallax (data-parallax)](#layer-4-parallax)
5. [Layer 5: Ambient CSS Effects](#layer-5-ambient-css-effects)
6. [Layer 6: Micro-interactions CSS](#layer-6-micro-interactions)
7. [Accessibility](#accessibility)
8. [Main Init (orchestrator)](#main-init)

---

## Layer 1: Smooth Scroll

**File: `smooth-scroll.js`**

```javascript
import Lenis from 'lenis';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

let lenis = null;

export function initSmoothScroll() {
  lenis = new Lenis({
    duration: 1.2,
    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
    smoothWheel: true,
    touchMultiplier: 1.5,
  });

  // Sync Lenis scroll position with GSAP ScrollTrigger
  lenis.on('scroll', ScrollTrigger.update);

  // Use GSAP ticker for Lenis RAF loop — this ensures both systems share the same frame timing
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });

  // Disable Lenis's own RAF since GSAP handles it
  gsap.ticker.lagSmoothing(0);

  return lenis;
}

export function getLenis() {
  return lenis;
}

export function pauseScroll() {
  if (lenis) lenis.stop();
}

export function resumeScroll() {
  if (lenis) lenis.start();
}

export function destroySmoothScroll() {
  if (lenis) {
    lenis.destroy();
    lenis = null;
  }
}
```

---

## Layer 2: Section Reveals

Curtain clip-path reveal: each `<section>` starts clipped and scales up as the user scrolls it into view.

**File: `section-reveals.js`**

```javascript
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export function initSectionReveals(selector = 'section') {
  const sections = document.querySelectorAll(selector);

  sections.forEach((section) => {
    // Set initial state via GSAP (not CSS, so it's cleaned up properly)
    gsap.set(section, {
      clipPath: 'inset(100% 0% 0% 0%)',
      scale: 0.96,
      opacity: 0.3,
    });

    gsap.to(section, {
      clipPath: 'inset(0% 0% 0% 0%)',
      scale: 1,
      opacity: 1,
      ease: 'power2.out',
      duration: 1,
      scrollTrigger: {
        trigger: section,
        start: 'top 95%',
        end: 'top 30%',
        scrub: 0.8,
        // Don't add 'once' here — scrub-based animations need continuous trigger
      },
    });
  });
}

export function destroySectionReveals() {
  ScrollTrigger.getAll().forEach((trigger) => {
    // Only kill triggers we created — check for section elements
    if (trigger.vars?.trigger?.tagName === 'SECTION') {
      trigger.kill();
    }
  });
}
```

---

## Layer 3: Entry Animations

The `data-anim` attribute system. Elements animate into view on first scroll.

**File: `entry-animations.js`**

```javascript
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

// Animation presets — define the "from" state for each type.
// GSAP animates FROM these values TO the element's natural state.
const ANIM_PRESETS = {
  up:    { y: 60, opacity: 0 },
  down:  { y: -50, opacity: 0 },
  left:  { x: 70, opacity: 0 },
  right: { x: -70, opacity: 0 },
  scale: { scale: 0.8, opacity: 0 },
  pop:   { scale: 0.75, rotation: -2, y: 40, opacity: 0 },
  blur:  { filter: 'blur(18px)', y: -30, scale: 0.95, opacity: 0 },
  clip:  { clipPath: 'inset(100% 0% 0% 0%)', opacity: 0 },
  wipe:  { clipPath: 'inset(0% 100% 0% 0%)', opacity: 0 },
};

// "To" overrides — only needed when the natural CSS state doesn't match the target
const ANIM_TO_OVERRIDES = {
  clip: { clipPath: 'inset(0% 0% 0% 0%)', opacity: 1 },
  wipe: { clipPath: 'inset(0% 0% 0% 0%)', opacity: 1 },
  blur: { filter: 'blur(0px)', y: 0, scale: 1, opacity: 1 },
};

export function initEntryAnimations(scope = document) {
  const elements = scope.querySelectorAll('[data-anim]');

  elements.forEach((el) => {
    const animType = el.dataset.anim;
    const delay = parseFloat(el.dataset.delay) || 0;
    const fromVars = ANIM_PRESETS[animType];

    if (!fromVars) {
      console.warn(`[sebanim8] Unknown data-anim value: "${animType}" on`, el);
      return;
    }

    // Set initial state immediately to prevent flash of unstyled content
    gsap.set(el, fromVars);

    const toVars = {
      ...{ y: 0, x: 0, scale: 1, rotation: 0, opacity: 1 },
      ...(ANIM_TO_OVERRIDES[animType] || {}),
      duration: 1.1,
      delay,
      ease: 'power3.out',
      scrollTrigger: {
        trigger: el,
        start: 'top 88%',
        toggleActions: 'play none none none', // play once on enter
      },
    };

    gsap.to(el, toVars);
  });
}

export function destroyEntryAnimations() {
  // Kill all ScrollTriggers tied to [data-anim] elements
  ScrollTrigger.getAll().forEach((trigger) => {
    if (trigger.vars?.trigger?.hasAttribute?.('data-anim')) {
      trigger.kill();
    }
  });
}
```

---

## Layer 4: Parallax

Elements with `data-parallax` shift vertically based on scroll position.

**File: `parallax.js`**

```javascript
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export function initParallax(scope = document) {
  const elements = scope.querySelectorAll('[data-parallax]');

  elements.forEach((el) => {
    const speed = parseFloat(el.dataset.parallax) || 0;

    // Clamp speed to safe range
    const clampedSpeed = Math.max(-0.12, Math.min(0.1, speed));
    const distance = clampedSpeed * 350;

    gsap.to(el, {
      y: distance,
      ease: 'none',
      scrollTrigger: {
        trigger: el,
        start: 'top bottom',
        end: 'bottom top',
        scrub: true,
      },
    });
  });
}

export function destroyParallax() {
  ScrollTrigger.getAll().forEach((trigger) => {
    if (trigger.vars?.trigger?.hasAttribute?.('data-parallax')) {
      trigger.kill();
    }
  });
}
```

---

## Layer 5: Ambient CSS Effects

Pure CSS — no JS required. Add these to your main stylesheet or a dedicated `sebanim8.css`.

**File: `sebanim8-ambient.css`**

```css
/* ============================================
   SEBANIM8 — Ambient CSS Effects
   ============================================ */

/* Global spring easing variable */
:root {
  --spring: cubic-bezier(0.16, 1, 0.3, 1);
}

/* --- Floating elements ---
   Apply class .seba-float to any badge, tag, or decorative element.
   Variants: .seba-float--slow (7s), .seba-float--fast (3s) */

@keyframes badge-float {
  0%, 100% {
    transform: translateY(0) rotate(0deg);
  }
  25% {
    transform: translateY(-10px) rotate(1.5deg);
  }
  50% {
    transform: translateY(-6px) rotate(-1deg);
  }
  75% {
    transform: translateY(-14px) rotate(2deg);
  }
}

.seba-float {
  animation: badge-float 5s ease-in-out infinite;
}
.seba-float--slow {
  animation-duration: 7s;
}
.seba-float--fast {
  animation-duration: 3s;
}

/* --- Glow drift ---
   Apply to background gradient containers.
   Slowly shifts background-position and pulses scale. */

@keyframes glow-drift {
  0%, 100% {
    background-position: 0% 50%;
    transform: scale(1);
  }
  33% {
    background-position: 100% 50%;
    transform: scale(1.03);
  }
  66% {
    background-position: 50% 100%;
    transform: scale(0.98);
  }
}

.seba-glow {
  background-size: 200% 200%;
  animation: glow-drift 10s ease-in-out infinite;
}
.seba-glow--fast {
  animation-duration: 6s;
}
.seba-glow--slow {
  animation-duration: 15s;
}

/* --- Pulse indicators ---
   For status dots, notification badges, live indicators. */

@keyframes seba-pulse {
  0%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.15);
    opacity: 0.7;
  }
}

.seba-pulse {
  animation: seba-pulse 2s ease-in-out infinite;
}
.seba-pulse--fast {
  animation-duration: 1.5s;
}
.seba-pulse--slow {
  animation-duration: 2.4s;
}

/* --- Particle system ---
   Container: .seba-particles (position: relative, overflow: hidden)
   Particles are generated via JS (see particle init below), but animated via CSS. */

@keyframes particle-float {
  0% {
    transform: translateY(0) translateX(0);
    opacity: 0;
  }
  10% {
    opacity: 0.6;
  }
  90% {
    opacity: 0.3;
  }
  100% {
    transform: translateY(-120vh) translateX(30px);
    opacity: 0;
  }
}

.seba-particle {
  position: absolute;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  animation: particle-float linear infinite;
  /* Individual particles get randomized:
     - width/height: 1-4px
     - bottom: random -10% to 10%
     - left: random 0-100%
     - animation-duration: 6-16s
     - animation-delay: random 0-8s
     - color: inherit from parent or set individually */
}

.seba-particles {
  position: relative;
  overflow: hidden;
}
```

### Particle Generator (JS helper)

This is the only JS needed for the ambient layer. Add to your init:

**File: `particles.js`**

```javascript
export function initParticles(containerSelector = '.seba-particles', count = 20) {
  const containers = document.querySelectorAll(containerSelector);

  containers.forEach((container) => {
    // Don't double-init
    if (container.dataset.particlesInit) return;
    container.dataset.particlesInit = 'true';

    const fragment = document.createDocumentFragment();

    for (let i = 0; i < count; i++) {
      const particle = document.createElement('span');
      particle.classList.add('seba-particle');

      const size = Math.random() * 3 + 1; // 1-4px
      const left = Math.random() * 100;     // 0-100%
      const bottom = Math.random() * 20 - 10; // -10% to 10%
      const duration = Math.random() * 10 + 6; // 6-16s
      const delay = Math.random() * 8;          // 0-8s

      particle.style.cssText = `
        width: ${size}px;
        height: ${size}px;
        left: ${left}%;
        bottom: ${bottom}%;
        animation-duration: ${duration}s;
        animation-delay: ${delay}s;
      `;

      fragment.appendChild(particle);
    }

    container.appendChild(fragment);
  });
}

export function destroyParticles(containerSelector = '.seba-particles') {
  const containers = document.querySelectorAll(containerSelector);
  containers.forEach((container) => {
    container.querySelectorAll('.seba-particle').forEach((p) => p.remove());
    delete container.dataset.particlesInit;
  });
}
```

---

## Layer 6: Micro-interactions

Pure CSS transitions for hover states, buttons, cards, and navigation links.

**File: `sebanim8-interactions.css`**

```css
/* ============================================
   SEBANIM8 — Micro-interactions
   ============================================ */

/* --- Buttons ---
   Apply .seba-btn for lift + shadow on hover. */

.seba-btn {
  transition: transform 0.2s var(--spring), box-shadow 0.2s var(--spring);
  will-change: transform;
}
.seba-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.seba-btn:active {
  transform: translateY(0);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* --- CTA Shimmer ---
   Apply .seba-shimmer to gradient-background CTAs.
   Requires background-size: 200% on the element. */

@keyframes seba-shimmer {
  0% {
    background-position: 200% center;
  }
  100% {
    background-position: -200% center;
  }
}

.seba-shimmer {
  background-size: 200% auto;
  animation: seba-shimmer 3.5s linear infinite;
}

/* --- Cards ---
   Apply .seba-card for scale + shadow elevation on hover. */

.seba-card {
  transition: transform 0.4s ease, box-shadow 0.4s ease;
  will-change: transform;
}
.seba-card:hover {
  transform: scale(1.03);
  box-shadow:
    0 12px 40px rgba(0, 0, 0, 0.12),
    0 4px 12px rgba(0, 0, 0, 0.08);
}

/* --- Navigation links ---
   Apply .seba-nav-link for expanding underline on hover. */

.seba-nav-link {
  position: relative;
  text-decoration: none;
}
.seba-nav-link::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background: currentColor;
  transition: width 0.25s var(--spring);
}
.seba-nav-link:hover::after {
  width: 100%;
}
```

---

## Accessibility

**Add this at the end of your main CSS file (or in `sebanim8-ambient.css`):**

```css
/* ============================================
   SEBANIM8 — Reduced Motion
   Respect user system preference: disable ALL animations.
   ============================================ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Reset GSAP-set initial states so content is visible */
  [data-anim] {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    filter: none !important;
  }

  section {
    clip-path: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}
```

---

## Main Init

Orchestrator that initializes all layers. Call once when the DOM is ready.

**File: `sebanim8-init.js`**

```javascript
import { initSmoothScroll, destroySmoothScroll } from './smooth-scroll';
import { initSectionReveals, destroySectionReveals } from './section-reveals';
import { initEntryAnimations, destroyEntryAnimations } from './entry-animations';
import { initParallax, destroyParallax } from './parallax';
import { initParticles, destroyParticles } from './particles';

// Check for reduced motion preference
const prefersReducedMotion = () =>
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

/**
 * Initialize all Sebanim8 animation layers.
 * Call after DOM is ready (DOMContentLoaded or equivalent).
 *
 * @param {Object} options
 * @param {boolean} options.smoothScroll - Enable Lenis smooth scroll (default: true)
 * @param {boolean} options.sectionReveals - Enable curtain section reveals (default: true)
 * @param {string}  options.sectionSelector - CSS selector for sections (default: 'section')
 * @param {boolean} options.entryAnimations - Enable data-anim entries (default: true)
 * @param {boolean} options.parallax - Enable data-parallax (default: true)
 * @param {boolean} options.particles - Enable particle system (default: true)
 * @param {string}  options.particleContainer - Particle container selector (default: '.seba-particles')
 * @param {number}  options.particleCount - Number of particles (default: 20)
 */
export function initSebanim8(options = {}) {
  const opts = {
    smoothScroll: true,
    sectionReveals: true,
    sectionSelector: 'section',
    entryAnimations: true,
    parallax: true,
    particles: true,
    particleContainer: '.seba-particles',
    particleCount: 20,
    ...options,
  };

  // Respect reduced motion — skip all JS animations
  if (prefersReducedMotion()) {
    console.info('[sebanim8] Reduced motion detected — animations disabled.');
    return;
  }

  if (opts.smoothScroll) initSmoothScroll();
  if (opts.sectionReveals) initSectionReveals(opts.sectionSelector);
  if (opts.entryAnimations) initEntryAnimations();
  if (opts.parallax) initParallax();
  if (opts.particles) initParticles(opts.particleContainer, opts.particleCount);
}

/**
 * Destroy all Sebanim8 layers. Call on unmount / cleanup.
 */
export function destroySebanim8() {
  destroySmoothScroll();
  destroySectionReveals();
  destroyEntryAnimations();
  destroyParallax();
  destroyParticles();
}
```

### Usage in main entry point

```javascript
// main.js or app.js
import { initSebanim8 } from './sebanim8-init';
import './sebanim8-ambient.css';
import './sebanim8-interactions.css';

document.addEventListener('DOMContentLoaded', () => {
  initSebanim8();
});
```

Or with specific layers only:

```javascript
initSebanim8({
  smoothScroll: true,
  sectionReveals: true,
  entryAnimations: true,
  parallax: false,       // skip parallax
  particles: false,      // skip particles
});
```
