VibeCoderzVibeCoderz
Telegram
All Prompts
ui componentlanding page

Image Trail

React UI компонент: минималистичная галерея с эффектом "image trail". Анимация движения, масштабирования, вращения. Идеально для лендингов.

by Zhou JasonLive Preview

Prompt

# Image Trail

You are given a task to integrate an existing React component in the codebase

~~~/README.md
# ImageTrail Showcase

A premium, interactive image trail animation system powered by GSAP. Designed for high-end digital experiences that require tactile, fluid interaction.

## Features
- **8 Distinct Animation Variants**: From basic lerp to 3D perspective and directional motion blur.
- **High Performance**: Optimized GSAP timelines with canvas-grade responsiveness.
- **Minimalist Aesthetic**: Clean editorial layout with floating containers and soft shadows.
- **Fully Responsive**: Works with mouse and touch events.

## Dependencies
- `gsap`: ^3.12.5
- `framer-motion`: ^11.0.0
- `lucide-react`: ^0.454.0

## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `items` | `string[]` | `[]` | Array of image URLs to display in the trail |
| `variant` | `number` | `1` | Animation style (1-8) |
| `threshold` | `number` | `80` | Distance in pixels mouse must move to trigger next image |

## Usage
```tsx
import { ImageTrail } from '@/sd-components/f308db27-073d-452f-95ac-7700415b625a';

function MyGallery() {
  const images = [
    'https://picsum.photos/id/101/600/600',
    'https://picsum.photos/id/102/600/600',
    // ...
  ];

  return (
    <div className="h-screen w-full relative overflow-hidden">
      <ImageTrail 
        items={images} 
        variant={2} 
        threshold={100} 
      />
    </div>
  );
}
```
~~~

~~~/src/App.tsx
import React, { useState } from 'react';
import { ImageTrail } from './Component';

/**
 * ImageTrailShowcase Demo
 * 
 * Demonstrates the premium minimalist showcase style for the ImageTrail effect.
 * Uses an editorial monochrome palette with a single accent color.
 */
export default function App() {
  const [variant, setVariant] = useState(1);
  const [key, setKey] = useState(0);

  const images = [
    'https://picsum.photos/id/287/600/600',
    'https://picsum.photos/id/1001/600/600',
    'https://picsum.photos/id/1025/600/600',
    'https://picsum.photos/id/1026/600/600',
    'https://picsum.photos/id/1027/600/600',
    'https://picsum.photos/id/1028/600/600',
    'https://picsum.photos/id/1029/600/600',
    'https://picsum.photos/id/1030/600/600',
    'https://picsum.photos/id/1031/600/600',
    'https://picsum.photos/id/1032/600/600',
    'https://picsum.photos/id/1033/600/600',
    'https://picsum.photos/id/1035/600/600',
  ];

  const variants = [
    { id: 1, name: 'Classic Lerp' },
    { id: 2, name: 'Luminous Bloom' },
    { id: 3, name: 'Ethereal Ascent' },
    { id: 4, name: 'Motion Drift' },
    { id: 5, name: 'Angular Flow' },
    { id: 6, name: 'Kinetic Blur' },
    { id: 7, name: 'Depth Stack' },
    { id: 8, name: '3D Perspective' },
  ];

  return (
    <div className="min-h-screen bg-[#F9F9F9] flex flex-col items-center justify-center p-20 font-sans text-[#1A1A1B]">
      {/* Editorial Header */}
      <div className="mb-12 text-center max-w-2xl">
        <h1 className="text-4xl font-medium tracking-tight mb-4">
          {variants.find(v => v.id === variant)?.name}
        </h1>
        <p className="text-muted-foreground font-light text-lg">
          Move your cursor across the frame to reveal the trail.
        </p>
      </div>

      {/* Main Canvas Container */}
      <div className="relative w-full max-w-4xl aspect-video bg-white rounded-[40px] shadow-[0_40px_100px_rgba(0,0,0,0.05)] border-none overflow-hidden cursor-crosshair group">
        <ImageTrail key={key} items={images} variant={variant} />
        
        {/* Subtle Branding/Instruction */}
        <div className="absolute bottom-10 left-10 opacity-0 group-hover:opacity-100 transition-opacity duration-500 text-[10px] uppercase tracking-[0.2em] font-medium text-muted-foreground pointer-events-none">
          Interactive Motion Showcase — v1.0
        </div>
      </div>

      {/* Navigation / Controls */}
      <div className="mt-16 flex flex-wrap justify-center gap-3">
        {variants.map((v) => (
          <button
            key={v.id}
            onClick={() => {
              setVariant(v.id);
              setKey(prev => prev + 1);
            }}
            className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-300 ${
              variant === v.id
                ? 'bg-[#1A1A1B] text-white shadow-lg'
                : 'bg-white text-[#1A1A1B] border border-transparent hover:border-[#1A1A1B] shadow-sm'
            }`}
          >
            {v.id}
          </button>
        ))}
        
        <button
          onClick={() => setKey(prev => prev + 1)}
          className="ml-4 px-6 py-2.5 rounded-full text-sm font-medium bg-[#3b82f6] text-white hover:bg-[#2563eb] transition-colors shadow-lg flex items-center gap-2"
        >
          <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
          </svg>
          Replay
        </button>
      </div>

      {/* Style Guide Enforcement Note */}
      <div className="fixed bottom-10 right-10 text-[10px] uppercase tracking-[0.3em] font-semibold text-[#1A1A1B]/20 pointer-events-none vertical-text">
        Minimalist Showcase
      </div>
    </div>
  );
}
~~~

~~~/package.json
{
  "name": "image-trail-showcase",
  "description": "A high-end interactive image trail component with multiple GSAP-powered animation variants.",
  "dependencies": {
    "gsap": "^3.12.5",
    "framer-motion": "^11.0.0",
    "lucide-react": "^0.454.0",
    "clsx": "^2.1.0",
    "tailwind-merge": "^2.2.1"
  }
}
~~~

~~~/src/Component.tsx
/**
 * ImageTrail Component
 * 
 * A high-performance image trail animation system powered by GSAP.
 * Supports multiple visual variants:
 * 1. Basic Lerp Trail
 * 2. Brightness & Scale Bloom
 * 3. Physics-like Upward Ejection
 * 4. Directional Motion Blur
 * 5. Rotational Trail
 * 6. Speed-based Size/Blur
 * 7. Infinite Stack Trail
 * 8. 3D Perspective Trail
 */

import React, { useRef, useEffect } from 'react';
import { gsap } from 'gsap';

function lerp(a: number, b: number, n: number): number {
  return (1 - n) * a + n * b;
}

function getLocalPointerPos(e: MouseEvent | TouchEvent, rect: DOMRect): { x: number; y: number } {
  let clientX = 0,
    clientY = 0;
  if ('touches' in e && e.touches.length > 0) {
    clientX = e.touches[0].clientX;
    clientY = e.touches[0].clientY;
  } else if ('clientX' in e) {
    const mouseEv = e as MouseEvent;
    clientX = mouseEv.clientX;
    clientY = mouseEv.clientY;
  }
  return {
    x: clientX - rect.left,
    y: clientY - rect.top
  };
}

function getMouseDistance(p1: { x: number; y: number }, p2: { x: number; y: number }): number {
  const dx = p1.x - p2.x;
  const dy = p1.y - p2.y;
  return Math.hypot(dx, dy);
}

class ImageItem {
  public DOM: { el: HTMLDivElement; inner: HTMLDivElement | null } = {
    el: null as unknown as HTMLDivElement,
    inner: null
  };
  public defaultStyle: gsap.TweenVars = { scale: 1, x: 0, y: 0, opacity: 0 };
  public rect: DOMRect | null = null;
  private resize!: () => void;

  constructor(DOM_el: HTMLDivElement) {
    this.DOM.el = DOM_el;
    this.DOM.inner = this.DOM.el.querySelector('.content__img-inner');
    this.getRect();
    this.initEvents();
  }

  private initEvents() {
    this.resize = () => {
      gsap.set(this.DOM.el, this.defaultStyle);
      this.getRect();
    };
    window.addEventListener('resize', this.resize);
  }

  private getRect() {
    this.rect = this.DOM.el.getBoundingClientRect();
  }

  public destroy() {
    window.removeEventListener('resize', this.resize);
  }
}

abstract class ImageTrailBase {
  protected container: HTMLDivElement;
  protected images: ImageItem[];
  protected imagesTotal: number;
  protected imgPosition: number = 0;
  protected zIndexVal: number = 1;
  protected activeImagesCount: number = 0;
  protected isIdle: boolean = true;
  protected threshold: number = 80;
  protected mousePos: { x: number; y: number } = { x: 0, y: 0 };
  protected lastMousePos: { x: number; y: number } = { x: 0, y: 0 };
  protected cacheMousePos: { x: number; y: number } = { x: 0, y: 0 };
  private rafId: number | null = null;

  constructor(container: HTMLDivElement) {
    this.container = container;
    this.images = [...container.querySelectorAll('.content__img')].map(img => new ImageItem(img as HTMLDivElement));
    this.imagesTotal = this.images.length;

    const handlePointerMove = (ev: MouseEvent | TouchEvent) => {
      const rect = this.container.getBoundingClientRect();
      this.mousePos = getLocalPointerPos(ev, rect);
    };

    container.addEventListener('mousemove', handlePointerMove);
    container.addEventListener('touchmove', handlePointerMove);

    const initRender = (ev: MouseEvent | TouchEvent) => {
      const rect = this.container.getBoundingClientRect();
      this.mousePos = getLocalPointerPos(ev, rect);
      this.cacheMousePos = { ...this.mousePos };
      this.rafId = requestAnimationFrame(() => this.render());
      container.removeEventListener('mousemove', initRender as EventListener);
      container.removeEventListener('touchmove', initRender as EventListener);
    };

    container.addEventListener('mousemove', initRender as EventListener);
    container.addEventListener('touchmove', initRender as EventListener);
  }

  protected abstract render(): void;
  protected abstract showNextImage(): void;

  protected onImageActivated() {
    this.activeImagesCount++;
    this.isIdle = false;
  }

  protected onImageDeactivated() {
    this.activeImagesCount--;
    if (this.activeImagesCount === 0) {
      this.isIdle = true;
    }
  }

  public destroy() {
    if (this.rafId) cancelAnimationFrame(this.rafId);
    this.images.forEach(img => img.destroy());
  }
}

class ImageTrailVariant1 extends ImageTrailBase {
  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    gsap.killTweensOf(img.DOM.el);
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      opacity: 1, scale: 1, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
    }, {
      duration: 0.4, ease: 'power1',
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2
    }, 0)
    .to(img.DOM.el, {
      duration: 0.4, ease: 'power3', opacity: 0, scale: 0.2
    }, 0.4);
  }
}

class ImageTrailVariant2 extends ImageTrailBase {
  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    gsap.killTweensOf(img.DOM.el);
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      opacity: 1, scale: 0, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
    }, {
      duration: 0.4, ease: 'power1', scale: 1,
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2
    }, 0)
    .fromTo(img.DOM.inner, { scale: 2.8, filter: 'brightness(250%)' }, {
      duration: 0.4, ease: 'power1', scale: 1, filter: 'brightness(100%)'
    }, 0)
    .to(img.DOM.el, {
      duration: 0.4, ease: 'power2', opacity: 0, scale: 0.2
    }, 0.45);
  }
}

class ImageTrailVariant3 extends ImageTrailBase {
  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    gsap.killTweensOf(img.DOM.el);
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      opacity: 1, scale: 0, zIndex: this.zIndexVal,
      xPercent: 0, yPercent: 0,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
    }, {
      duration: 0.4, ease: 'power1', scale: 1,
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2
    }, 0)
    .to(img.DOM.el, {
      duration: 0.6, ease: 'power2', opacity: 0, scale: 0.2,
      xPercent: () => gsap.utils.random(-30, 30),
      yPercent: -200
    }, 0.6);
  }
}

class ImageTrailVariant4 extends ImageTrailBase {
  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    gsap.killTweensOf(img.DOM.el);
    
    let dx = this.mousePos.x - this.cacheMousePos.x;
    let dy = this.mousePos.y - this.cacheMousePos.y;
    let dist = Math.sqrt(dx * dx + dy * dy);
    if (dist !== 0) { dx /= dist; dy /= dist; }
    dx *= dist / 100;
    dy *= dist / 100;

    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      opacity: 1, scale: 0, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
    }, {
      duration: 0.4, ease: 'power1', scale: 1,
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2
    }, 0)
    .fromTo(img.DOM.inner, {
      scale: 2,
      filter: `brightness(${Math.max((400 * dist) / 100, 100)}%) contrast(${Math.max((400 * dist) / 100, 100)}%)`
    }, {
      duration: 0.4, ease: 'power1', scale: 1, filter: 'brightness(100%) contrast(100%)'
    }, 0)
    .to(img.DOM.el, { duration: 0.4, ease: 'power3', opacity: 0 }, 0.4)
    .to(img.DOM.el, { duration: 1.5, ease: 'power4', x: `+=${dx * 110}`, y: `+=${dy * 110}` }, 0.05);
  }
}

class ImageTrailVariant5 extends ImageTrailBase {
  private lastAngle: number = 0;

  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    let dx = this.mousePos.x - this.cacheMousePos.x;
    let dy = this.mousePos.y - this.cacheMousePos.y;
    let angle = Math.atan2(dy, dx) * (180 / Math.PI);
    if (angle < 0) angle += 360;
    if (angle > 90 && angle <= 270) angle += 180;
    const isMovingClockwise = angle >= this.lastAngle;
    this.lastAngle = angle;
    let startAngle = isMovingClockwise ? angle - 10 : angle + 10;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist !== 0) { dx /= dist; dy /= dist; }
    dx *= dist / 150; dy *= dist / 150;

    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    gsap.killTweensOf(img.DOM.el);
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      opacity: 1, filter: 'brightness(80%)', scale: 0.1, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,
      rotation: startAngle
    }, {
      duration: 1, ease: 'power2', scale: 1, filter: 'brightness(100%)',
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2 + dx * 70,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2 + dy * 70,
      rotation: this.lastAngle
    }, 0)
    .to(img.DOM.el, { duration: 0.4, ease: 'expo', opacity: 0 }, 0.5)
    .to(img.DOM.el, { duration: 1.5, ease: 'power4', x: `+=${dx * 120}`, y: `+=${dy * 120}` }, 0.05);
  }
}

class ImageTrailVariant6 extends ImageTrailBase {
  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    const dx = this.mousePos.x - this.cacheMousePos.x;
    const dy = this.mousePos.y - this.cacheMousePos.y;
    const speed = Math.sqrt(dx * dx + dy * dy);
    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    
    const scaleFactor = 0.3 + (1.7) * Math.min(speed / 200, 1);
    const brightnessValue = 1 + (0.3) * Math.min(speed / 70, 1);
    const blurValue = 20 * (1 - Math.min(speed / 90, 1));
    const grayscaleValue = 1 * (1 - Math.min(speed / 90, 1));

    gsap.killTweensOf(img.DOM.el);
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      opacity: 1, scale: 0, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
    }, {
      duration: 0.8, ease: 'power3', scale: scaleFactor,
      filter: `grayscale(${grayscaleValue * 100}%) brightness(${brightnessValue * 100}%) blur(${blurValue}px)`,
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2
    }, 0)
    .to(img.DOM.el, { duration: 0.4, ease: 'power3.in', opacity: 0, scale: 0.2 }, 0.45);
  }
}

class ImageTrailVariant7 extends ImageTrailBase {
  private visibleImagesCount: number = 0;
  private visibleImagesTotal: number = 9;

  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.3);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.3);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    ++this.visibleImagesCount;
    gsap.killTweensOf(img.DOM.el);
    const scaleValue = gsap.utils.random(0.5, 1.6);
    
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .fromTo(img.DOM.el, {
      scale: scaleValue - 0.4, rotationZ: 0, opacity: 1, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2
    }, {
      duration: 0.4, ease: 'power3', scale: scaleValue,
      rotationZ: gsap.utils.random(-3, 3),
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2
    }, 0);

    if (this.visibleImagesCount >= this.visibleImagesTotal) {
      const lastInQueue = (this.imgPosition - this.visibleImagesTotal + this.imagesTotal) % this.imagesTotal;
      const oldImg = this.images[lastInQueue];
      gsap.to(oldImg.DOM.el, { duration: 0.4, ease: 'power4', opacity: 0, scale: 1.3 });
    }
  }
}

class ImageTrailVariant8 extends ImageTrailBase {
  protected render() {
    const distance = getMouseDistance(this.mousePos, this.lastMousePos);
    this.cacheMousePos.x = lerp(this.cacheMousePos.x, this.mousePos.x, 0.1);
    this.cacheMousePos.y = lerp(this.cacheMousePos.y, this.mousePos.y, 0.1);
    if (distance > this.threshold) {
      this.showNextImage();
      this.lastMousePos = { ...this.mousePos };
    }
    if (this.isIdle && this.zIndexVal !== 1) this.zIndexVal = 1;
    requestAnimationFrame(() => this.render());
  }

  protected showNextImage() {
    const rect = this.container.getBoundingClientRect();
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;
    const relX = this.mousePos.x - centerX;
    const relY = this.mousePos.y - centerY;
    
    const rotX = -(relY / centerY) * 30;
    const rotY = (relX / centerX) * 30;
    const distFromCenter = Math.sqrt(relX * relX + relY * relY);
    const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
    const zValue = (distFromCenter / maxDist) * 1200 - 600;
    const brightness = 0.2 + ((zValue + 600) / 1200) * 2.3;

    ++this.zIndexVal;
    this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
    const img = this.images[this.imgPosition];
    gsap.killTweensOf(img.DOM.el);
    gsap.timeline({
      onStart: () => this.onImageActivated(),
      onComplete: () => this.onImageDeactivated()
    })
    .set(this.container, { perspective: 1000 })
    .fromTo(img.DOM.el, {
      opacity: 1, z: 0, scale: 1 + zValue / 1000, zIndex: this.zIndexVal,
      x: this.cacheMousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.cacheMousePos.y - (img.rect?.height ?? 0) / 2,
      rotationX: rotX, rotationY: rotY, filter: `brightness(${brightness})`
    }, {
      duration: 1, ease: 'expo', scale: 1 + zValue / 1000,
      x: this.mousePos.x - (img.rect?.width ?? 0) / 2,
      y: this.mousePos.y - (img.rect?.height ?? 0) / 2,
      rotationX: rotX, rotationY: rotY
    }, 0)
    .to(img.DOM.el, { duration: 0.4, ease: 'power2', opacity: 0, z: -800 }, 0.3);
  }
}

const variantMap: Record<number, any> = {
  1: ImageTrailVariant1,
  2: ImageTrailVariant2,
  3: ImageTrailVariant3,
  4: ImageTrailVariant4,
  5: ImageTrailVariant5,
  6: ImageTrailVariant6,
  7: ImageTrailVariant7,
  8: ImageTrailVariant8
};

export interface ImageTrailProps {
  /** Array of image URLs to trail */
  items: string[];
  /** Animation variant (1-8) */
  variant?: number;
  /** Custom threshold for triggering next image (distance in px) */
  threshold?: number;
}

/**
 * ImageTrail Component
 * 
 * Renders a sequence of images that follow the cursor with various animation effects.
 * Perfect for landing pages, portfolio showcases, or editorial interactive sections.
 */
export function ImageTrail({ items = [], variant = 1, threshold = 80 }: ImageTrailProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const instanceRef = useRef<any>(null);

  useEffect(() => {
    if (!containerRef.current || items.length === 0) return;

    const Cls = variantMap[variant] || variantMap[1];
    instanceRef.current = new Cls(containerRef.current);
    
    // Set threshold if custom
    if (instanceRef.current && threshold !== 80) {
      instanceRef.current.threshold = threshold;
    }

    return () => {
      if (instanceRef.current) {
        instanceRef.current.destroy();
      }
    };
  }, [variant, items, threshold]);

  return (
    <div 
      className="w-full h-full relative z-[100] rounded-lg bg-transparent overflow-visible touch-none" 
      ref={containerRef}
    >
      {items.map((url, i) => (
        <div
          className="content__img w-[190px] aspect-[1.1] rounded-[15px] absolute top-0 left-0 opacity-0 overflow-hidden [will-change:transform,filter] shadow-2xl"
          key={i}
        >
          <div
            className="content__img-inner bg-center bg-cover w-[calc(100%+20px)] h-[calc(100%+20px)] absolute top-[-10px] left-[-10px]"
            style={{ backgroundImage: `url(${url})` }}
          />
        </div>
      ))}
    </div>
  );
}

export default ImageTrail;
~~~

Implementation Guidelines

1. Analyze the component structure, styling, animation implementations
2. Review the component's arguments and state
3. Think through what is the best place to adopt this component/style into the design we are doing
4. Then adopt the component/design to our current system

Help me integrate this into my design
All Prompts