Team 2
Team 2 is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
other
What it is
Team 2 is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Team 2
"use client";
import { getAllPeople, getAvatarUrl, type Person } from "@/lib/smoothui-data";
import { motion, useReducedMotion } from "motion/react";
import { useEffect, useState } from "react";
const CARDS_PER_VIEW = 3; // Number of cards visible at once
const CARD_WIDTH = 288;
const CARD_GAP = 16;
const AUTOPLAY_INTERVAL = 5000;
const TRANSITION_TIMEOUT = 1500;
const AVATAR_SIZE = 160;
const STAGGER_DELAY = 0.1;
interface TeamCarouselProps {
description?: string;
members?: Person[];
subtitle?: string;
title?: string;
}
export function TeamCarousel({
title = "Tech Pioneers",
subtitle = "building the future",
description = "We bring together brilliant developers, engineers, and tech innovators to create groundbreaking digital solutions.",
members = getAllPeople(),
}: TeamCarouselProps) {
const shouldReduceMotion = useReducedMotion();
const [currentIndex, setCurrentIndex] = useState(0);
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
const [isTransitioning, setIsTransitioning] = useState(false);
useEffect(() => {
if (!isAutoPlaying || isTransitioning) {
return;
}
const interval = setInterval(() => {
setCurrentIndex(
(prev) => (prev + 1) % (members.length - CARDS_PER_VIEW + 1)
);
}, AUTOPLAY_INTERVAL);
return () => clearInterval(interval);
}, [members.length, isAutoPlaying, isTransitioning]);
const nextSlide = () => {
if (isTransitioning) {
return;
}
const maxIndex = members.length - CARDS_PER_VIEW;
if (currentIndex >= maxIndex) {
return;
}
setIsTransitioning(true);
setCurrentIndex((prev) => Math.min(prev + 1, maxIndex));
setIsAutoPlaying(false);
setTimeout(() => {
setIsTransitioning(false);
setIsAutoPlaying(true);
}, TRANSITION_TIMEOUT);
};
const prevSlide = () => {
if (isTransitioning) {
return;
}
if (currentIndex <= 0) {
return;
}
setIsTransitioning(true);
setCurrentIndex((prev) => Math.max(prev - 1, 0));
setIsAutoPlaying(false);
setTimeout(() => {
setIsTransitioning(false);
setIsAutoPlaying(true);
}, TRANSITION_TIMEOUT);
};
return (
<section className="overflow-hidden py-32">
<div className="mx-auto max-w-5xl px-8 lg:px-0">
<motion.div
initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}
viewport={{ once: true }}
whileInView={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
>
<h2 className="font-medium text-5xl md:text-6xl">
{title} <br />
<span className="text-foreground/50">{subtitle}</span>
</h2>
<p className="mt-6 max-w-md text-foreground/70">{description}</p>
</motion.div>
<div className="relative">
{/* Navigation Buttons */}
<div className="mt-4 hidden items-center justify-end gap-4 md:flex">
<motion.button
className="static top-1/2 -left-12 inline-flex size-11 shrink-0 translate-x-0 translate-y-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full border bg-background font-medium text-sm shadow-xs outline-none transition-all hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
disabled={currentIndex === 0 || isTransitioning}
onClick={prevSlide}
type="button"
whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
>
<svg
aria-hidden="true"
className="lucide lucide-arrow-left"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>
<span className="sr-only">Previous slide</span>
</motion.button>
<motion.button
className="static top-1/2 -right-12 inline-flex size-11 shrink-0 translate-x-0 translate-y-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full border bg-background font-medium text-sm shadow-xs outline-none transition-all hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
disabled={
currentIndex >= members.length - CARDS_PER_VIEW ||
isTransitioning
}
onClick={nextSlide}
type="button"
whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
>
<svg
aria-hidden="true"
className="lucide lucide-arrow-right"
fill="none"
height="24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
<span className="sr-only">Next slide</span>
</motion.button>
</div>
{/* Carousel Content */}
<div className="mt-16 [&>div[data-slot=carousel-content]]:overflow-visible">
<div className="overflow-hidden" data-slot="carousel-content">
<motion.div
animate={{ x: `-${currentIndex * (CARD_WIDTH + CARD_GAP)}px` }}
className="-ml-4 flex max-w-[min(calc(100vw-4rem),24rem)] select-none"
transition={
shouldReduceMotion
? { duration: 0 }
: { damping: 30, stiffness: 300, type: "spring" as const }
}
>
{members.map((member, index) => (
<div
className="min-w-0 max-w-72 shrink-0 grow-0 basis-full pl-4"
data-slot="carousel-item"
key={member.name}
>
<motion.div
className="rounded-2xl border border-border bg-background p-7 text-center"
initial={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 0, y: 20 }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { delay: index * STAGGER_DELAY, duration: 0.5 }
}
viewport={{ once: true }}
whileInView={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, y: 0 }
}
>
<img
alt={member.name}
className="mx-auto size-20 rounded-full border border-border"
draggable={false}
height={80}
src={getAvatarUrl(member.avatar, AVATAR_SIZE)}
width={80}
/>
<div className="mt-6 flex flex-col justify-center">
<p className="font-medium text-foreground text-lg">
{member.name}
</p>
<p className="text-muted-foreground text-sm">
{member.role}
</p>
</div>
<div
className="my-6 shrink-0 bg-border bg-linear-to-r from-background via-border to-background data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px"
data-orientation="horizontal"
data-slot="separator-root"
role="none"
/>
<p className="text-muted-foreground text-sm">
{member.experience}
</p>
</motion.div>
</div>
))}
</motion.div>
</div>
</div>
</div>
</div>
</section>
);
}
export default TeamCarousel;