Team Profile animation using GSAP
A team profile animation component
/* 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>
)
}