Team Profile animation using GSAP

A team profile animation component

Live Preview
Open in
Karan
Liam
Daisy
Tyson
Max
Evrest
Simons
Mark
Mannu

Team

Karan

Liam

Daisy

Tyson

Max

Evrest

Simons

Mark

Mannu

Gsap
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
"use client"

import { useEffect, useRef, useState } from "react"
import Image from "next/image"
import { scale } from "framer-motion"

// Define types directly in the file
interface TeamMember {
  id: number
  name: string
  image: string
}

// Include team members data directly in the component file
const teamMembers: TeamMember[] = [
  {
    id: 1,
    name: "Karan",
    image: "/img1.png",
  },
  {
    id: 2,
    name: "Liam",
    image: "/img2.png",
  },
  {
    id: 3,
    name: "Daisy",
    image: "/img3.png",
  },
  {
    id: 4,
    name: "Tyson",
    image: "/img4.png",
  },
  {
    id: 5,
    name: "Max",
    image: "/img5.png",
  },
  {
    id: 6,
    name: "Evrest",
    image: "/img6.png",
  },
  {
    id: 7,
    name: "Simons",
    image: "/img7.png",
  },
  {
    id: 8,
    name: "Mark",
    image: "/img8.png",
  },
  {
    id: 9,
    name: "Mannu",
    image: "/img9.png",
  },
]

export default function TeamShowcase() {
  const profileImagesRef = useRef<HTMLDivElement>(null)
  const nameElementsRef = useRef<(HTMLDivElement | null)[]>([])
  const nameHeadingsRef = useRef<(HTMLHeadingElement | null)[]>([])
  const [gsapLoaded, setGsapLoaded] = useState(false)

  useEffect(() => {
    // Dynamically import GSAP and SplitText to avoid SSR issues
    const loadGsap = async () => {
      try {
        const gsapModule = await import("gsap")
        const SplitTextModule = await import("gsap/SplitText")

        const gsap = gsapModule.default || gsapModule
        const SplitText = SplitTextModule.default || SplitTextModule.SplitText

        // Register SplitText plugin
        gsap.registerPlugin(SplitText)
        setGsapLoaded(true)

        // Initialize animations after GSAP is loaded
        initializeAnimations(gsap, SplitText)
      } catch (error) {
        console.error("Failed to load GSAP:", error)
      }
    }

    loadGsap()

    // Cleanup function
    return () => {
      // Cleanup will be handled inside initializeAnimations
    }
  }, [])

  const initializeAnimations = (gsap: any, SplitText: any) => {
    // Store split instances for cleanup
    const splitInstances: any[] = []

    // Process each heading with null checks
    nameHeadingsRef.current.forEach((heading) => {
      if (heading) {
        try {
          // Create split text instance
          const split = new SplitText(heading, { type: "chars" })

          // Add letter class to each character if chars exists
          if (split.chars && Array.isArray(split.chars)) {
            split.chars.forEach((char: Element) => {
              if (char) char.classList.add("letter")
            })
          }

          splitInstances.push(split)
        } catch (error) {
          console.error("Error splitting text:", error)
        }
      }
    })

    // Set default letters to be hidden initially
    const defaultElement = nameElementsRef.current[0]
    const defaultLetters = defaultElement?.querySelectorAll(".letter")
    if (defaultLetters && defaultLetters.length > 0) {
      gsap.set(defaultLetters, { y: "100%" })
    }

    // Only apply hover animations on desktop
    if (typeof window !== "undefined" && window.innerWidth > 900) {
      const profileImages = document.querySelectorAll(".profile-image-item")

      profileImages.forEach((img, index) => {
        const nameElement = nameElementsRef.current[index + 1]
        if (!nameElement) return

        const letters = nameElement.querySelectorAll(".letter")
        if (!letters || letters.length === 0) return

        img.addEventListener("mouseenter", () => {
          gsap.to(img, {
            width: 140,
            height: 140,
            duration: 0.5,
            ease: "power4.out",
          })

          gsap.to(letters, {
            y: "-100%",
            duration: 0.75,
            ease: "power4.out",
            stagger: {
              each: 0.025,
              from: "center",
            },
          })
        })

        img.addEventListener("mouseleave", () => {
          gsap.to(img, {
            width: 70,
            height: 70,
            duration: 0.5,
            ease: "power4.out",
          })

          gsap.to(letters, {
            y: "0%",
            duration: 0.75,
            ease: "power4.out",
            stagger: {
              each: 0.025,
              from: "center",
            },
          })
        })
      })

      // Handle hover on the entire profile images container
      const profileImagesContainer = profileImagesRef.current
      if (profileImagesContainer && defaultLetters && defaultLetters.length > 0) {
        profileImagesContainer.addEventListener("mouseenter", () => {
          gsap.to(defaultLetters, {
            y: "0%",
            duration: 0.75,
            ease: "power4.out",
            stagger: {
              each: 0.025,
              from: "center",
            },
          })
        })

        profileImagesContainer.addEventListener("mouseleave", () => {
          gsap.to(defaultLetters, {
            y: "100%",
            duration: 0.75,
            ease: "power4.out",
            stagger: {
              each: 0.025,
              from: "center",
            },
          })
        })
      }
    }

    // Return cleanup function
    return () => {
      splitInstances.forEach((split) => {
        if (split && typeof split.revert === "function") {
          split.revert()
        }
      })
    }
  }

  return (
    <section className="relative w-screen h-screen bg-[#0f0f0f] text-[#e3e3db] flex flex-col justify-center items-center gap-10 overflow-hidden md:flex-col">
      <div
        ref={profileImagesRef}
        className="w-max flex justify-center items-center flex-wrap max-w-[90%] md:flex-nowrap"
      >
        {teamMembers.map((member, index) => (
          <div
            key={member.id}
            className="profile-image-item relative w-[60px] h-[60px] md:w-[70px] md:h-[70px] p-[2.5px] md:p-[5px] cursor-pointer will-change-[width,height]"
          >
            <Image
              src={member.image || "/placeholder.svg"}
              alt={member.name}
              width={140}
              height={140}
              className="rounded-lg object-cover w-full h-full"
            />
          </div>
        ))}
      </div>

      <div className="w-full h-[4rem] md:h-[20rem] overflow-hidden [clip-path:polygon(0_0,100%_0,100%_100%,0%_100%)]">
        <div ref={(el) => (nameElementsRef.current[0] = el)} className="name default">
          <h1
            ref={(el) => (nameHeadingsRef.current[0] = el)}
            className="absolute w-full text-center uppercase font-['Barlow_Condensed'] text-xl md:text-[20rem] font-black md:tracking-[-0.5rem] leading-none text-[#e3e3e3] select-none transform translate-y-[-100%]"
          >
            Team
          </h1>
        </div>

        {teamMembers.map((member, index) => (
          <div key={member.id} ref={(el) => (nameElementsRef.current[index + 1] = el)} className="name">
            <h1
              ref={(el) => (nameHeadingsRef.current[index + 1] = el)}
              className="absolute w-full text-center uppercase font-['Barlow_Condensed'] text-4xl md:text-[20rem] font-black md:tracking-[-0.5rem] leading-none text-[#f93535] select-none transform translate-y-[100%]"
            >
              {member.name}
            </h1>
          </div>
        ))}
      </div>
    </section>
  )
}