"use client";
import * as React from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";
import { AtSign, Globe, Mail, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
export type TeamMember = {
name: string;
role: string;
bio: string;
location?: string;
/** Tailwind gradient classes for the avatar, e.g. "from-sky-500 to-indigo-600". */
gradient?: string;
links?: { email?: string; website?: string; handle?: string };
};
export interface TeamGridProps {
eyebrow?: string;
title?: string;
subtitle?: string;
members?: TeamMember[];
cta?: { label: string; href: string } | null;
className?: string;
}
const GRADIENTS = [
"from-violet-500 via-fuchsia-500 to-pink-500",
"from-sky-500 via-blue-500 to-indigo-600",
"from-emerald-400 via-teal-500 to-cyan-600",
"from-amber-400 via-orange-500 to-rose-500",
"from-rose-500 via-pink-500 to-fuchsia-600",
"from-cyan-400 via-sky-500 to-blue-600",
"from-lime-400 via-emerald-500 to-teal-600",
"from-indigo-500 via-violet-500 to-purple-600",
];
const DEFAULT_MEMBERS: TeamMember[] = [
{ name: "Ava Lindgren", role: "Co-founder & CEO", location: "Stockholm", bio: "Previously led product at two fintech scale-ups. Obsessed with calm software and long walks.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Mateo Rivera", role: "Co-founder & CTO", location: "Lisbon", bio: "Built distributed systems for a decade. Writes Rust for fun and bakes sourdough for friends.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Noor Haddad", role: "Head of Design", location: "Amman", bio: "Designs interfaces that disappear. Former type designer with a soft spot for grids.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Kai Nakamura", role: "Lead Engineer", location: "Osaka", bio: "Performance nerd who shaved 60% off our bundle. Runs the internal tech talks.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Zara Okonkwo", role: "Product Manager", location: "Lagos", bio: "Turns fuzzy customer problems into crisp roadmaps. Talks to five users every week.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Leo Brandt", role: "Customer Success", location: "Berlin", bio: "Our customers' favourite human. Answers tickets faster than most people read them.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Isla Moreau", role: "Marketing Lead", location: "Montréal", bio: "Storyteller and former journalist. Makes technical launches sound like good news.", links: { email: "[email protected]", website: "#", handle: "#" } },
{ name: "Ravi Menon", role: "Data Scientist", location: "Bengaluru", bio: "Forecasts, experiments and the occasional chess tournament. Loves a good histogram.", links: { email: "[email protected]", website: "#", handle: "#" } },
];
function initials(name: string) {
return name
.split(/\s+/)
.map((p) => p[0])
.slice(0, 2)
.join("")
.toUpperCase();
}
const container: Variants = { hidden: {}, show: { transition: { staggerChildren: 0.07, delayChildren: 0.05 } } };
const card: Variants = {
hidden: { opacity: 0, y: 28, scale: 0.96 },
show: { opacity: 1, y: 0, scale: 1, transition: { type: "spring", stiffness: 260, damping: 26 } },
};
function MemberCard({ m, index }: { m: TeamMember; index: number }) {
const reduce = useReducedMotion();
const [hover, setHover] = React.useState(false);
const [focusWithin, setFocusWithin] = React.useState(false);
const [pinned, setPinned] = React.useState(false); // tap/click toggle for touch devices
const flipped = hover || focusWithin || pinned;
const gradient = m.gradient ?? GRADIENTS[index % GRADIENTS.length];
const face = "absolute inset-0 overflow-hidden rounded-3xl [backface-visibility:hidden] [-webkit-backface-visibility:hidden]";
return (
<motion.li variants={reduce ? undefined : card} className="[perspective:1200px]">
<div
className="relative aspect-[3/4] w-full sm:aspect-[4/5] lg:aspect-[10/11]"
onPointerEnter={(e) => e.pointerType === "mouse" && setHover(true)}
onPointerLeave={(e) => e.pointerType === "mouse" && setHover(false)}
onFocus={(e) => {
// keyboard focus flips the card; taps are handled by the "Read bio" toggle instead
if ((e.target as HTMLElement).matches(":focus-visible")) setFocusWithin(true);
}}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocusWithin(false);
}}
>
<motion.div
className="relative size-full [transform-style:preserve-3d]"
initial={false}
animate={reduce ? { opacity: 1 } : { rotateY: flipped ? 180 : 0 }}
transition={{ type: "spring", stiffness: 180, damping: 22 }}
>
{/* Front */}
<div className={cn(face, "border bg-card", reduce && flipped && "invisible")}>
<div className={cn("absolute inset-0 bg-gradient-to-br opacity-90", gradient)} />
<div aria-hidden className="absolute inset-0 [background-image:radial-gradient(rgb(255_255_255/0.22)_1px,transparent_1px)] [background-size:14px_14px] [mask-image:linear-gradient(to_bottom,#000,transparent_70%)]" />
<div className="absolute inset-x-0 top-[14%] flex justify-center lg:top-[12%]">
<span aria-hidden className="grid size-20 place-items-center rounded-full bg-white/20 text-2xl font-semibold tracking-tight text-white shadow-xl ring-1 ring-white/40 backdrop-blur sm:size-24 sm:text-3xl">
{initials(m.name)}
</span>
</div>
<div className="absolute inset-x-2 bottom-2 rounded-2xl bg-card/95 p-3 backdrop-blur sm:inset-x-3 sm:bottom-3 sm:p-4">
<h3 className="truncate text-sm font-semibold text-foreground sm:text-base">{m.name}</h3>
<p className="truncate text-xs text-muted-foreground sm:text-sm">{m.role}</p>
<button
type="button"
onClick={() => setPinned((p) => !p)}
aria-expanded={flipped}
className="mt-2 text-xs font-medium text-primary underline-offset-4 hover:underline focus-visible:rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Read bio<span className="sr-only"> of {m.name}</span>
</button>
</div>
</div>
{/* Back */}
<div
className={cn(
face,
"flex flex-col border bg-card p-4 text-card-foreground sm:p-5",
reduce ? (flipped ? "visible" : "invisible") : "[transform:rotateY(180deg)]",
)}
>
<div aria-hidden className={cn("absolute -right-10 -top-10 size-32 rounded-full bg-gradient-to-br opacity-25 blur-2xl", gradient)} />
<div className="relative flex items-center gap-3">
<span aria-hidden className={cn("grid size-10 shrink-0 place-items-center rounded-full bg-gradient-to-br text-sm font-semibold text-white", gradient)}>
{initials(m.name)}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-semibold">{m.name}</p>
{m.location && <p className="truncate text-xs text-muted-foreground">{m.location}</p>}
</div>
</div>
<p className="relative mt-3 truncate text-[11px] font-semibold uppercase tracking-wider text-primary sm:mt-5 sm:text-xs">{m.role}</p>
<p className="relative mt-1.5 line-clamp-4 text-xs leading-relaxed text-muted-foreground sm:mt-2 sm:line-clamp-none sm:text-sm">{m.bio}</p>
<div className="relative mt-auto flex items-center gap-1.5 pt-3">
{m.links?.email && (
<a href={`mailto:${m.links.email}`} aria-label={`Email ${m.name}`} className="grid size-8 place-items-center rounded-full border bg-background text-muted-foreground transition hover:border-primary/40 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Mail className="size-3.5" />
</a>
)}
{m.links?.website && (
<a href={m.links.website} aria-label={`${m.name}'s website`} className="grid size-8 place-items-center rounded-full border bg-background text-muted-foreground transition hover:border-primary/40 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Globe className="size-3.5" />
</a>
)}
{m.links?.handle && (
<a href={m.links.handle} aria-label={`${m.name}'s profile`} className="grid size-8 place-items-center rounded-full border bg-background text-muted-foreground transition hover:border-primary/40 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<AtSign className="size-3.5" />
</a>
)}
{pinned && (
<button
type="button"
onClick={() => setPinned(false)}
aria-label={`Hide bio of ${m.name}`}
className="ml-auto grid size-8 place-items-center rounded-full text-muted-foreground transition hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<RotateCcw className="size-3.5" />
</button>
)}
</div>
</div>
</motion.div>
</div>
</motion.li>
);
}
export function TeamGrid({
eyebrow = "Our team",
title = "Small team, big ambitions",
subtitle = "We're a remote-first crew of builders across eight time zones. Hover or focus a card to meet us properly.",
members = DEFAULT_MEMBERS,
cta = { label: "We're hiring — see open roles", href: "#careers" },
className,
}: TeamGridProps) {
const reduce = useReducedMotion();
return (
<section className={cn("relative w-full overflow-hidden bg-background px-4 py-16 sm:px-6 sm:py-20", className)}>
<div className="mx-auto max-w-6xl">
<header className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
<div className="max-w-2xl">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-primary">{eyebrow}</p>
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-5xl">{title}</h2>
<p className="mt-4 text-pretty text-base text-muted-foreground sm:text-lg">{subtitle}</p>
</div>
{cta && (
<a
href={cta.href}
className="inline-flex h-11 shrink-0 items-center gap-2 self-start rounded-full border bg-card px-5 text-sm font-semibold text-foreground transition hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:self-auto"
>
<span className="relative flex size-2">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75 motion-reduce:animate-none" />
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
</span>
{cta.label}
</a>
)}
</header>
<motion.ul
variants={reduce ? undefined : container}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-60px" }}
className="mt-12 grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-4"
>
{members.map((m, i) => (
<MemberCard key={m.name} m={m} index={i} />
))}
</motion.ul>
</div>
</section>
);
}