"use client"; import { ArrowUpRightIcon, AtSignIcon, GlobeIcon, MailIcon, PlusIcon } from "lucide-react"; import { AnimatePresence, motion, MotionConfig, useReducedMotion } from "motion/react"; import { type ReactNode, useEffect, useId, useState } from "react"; import { cn } from "@/lib/utils"; function useReducedMotionSafe() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && Boolean(prefers); } export type TeamTone = "violet" | "sky" | "emerald" | "amber" | "rose" | "teal" | "indigo" | "orange"; export interface TeamMember { name: string; role: string; team: string; location: string; timeZone?: string; bio: string; tone?: TeamTone; links?: { email?: string; website?: string; handle?: string }; } export interface TeamGridProps { eyebrow?: string; title?: ReactNode; description?: ReactNode; members: TeamMember[]; hiring?: { title: string; text: string; href: string; count?: number }; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const tones: Record = { violet: { bg: "from-violet-100 to-violet-50/60 dark:from-violet-500/25 dark:to-violet-500/5", text: "text-violet-700 dark:text-violet-300", ring: "text-violet-500/25 dark:text-violet-300/15", }, sky: { bg: "from-sky-100 to-sky-50/60 dark:from-sky-500/25 dark:to-sky-500/5", text: "text-sky-700 dark:text-sky-300", ring: "text-sky-500/25 dark:text-sky-300/15", }, emerald: { bg: "from-emerald-100 to-emerald-50/60 dark:from-emerald-500/25 dark:to-emerald-500/5", text: "text-emerald-700 dark:text-emerald-300", ring: "text-emerald-500/25 dark:text-emerald-300/15", }, amber: { bg: "from-amber-100 to-amber-50/60 dark:from-amber-500/25 dark:to-amber-500/5", text: "text-amber-800 dark:text-amber-300", ring: "text-amber-500/30 dark:text-amber-300/15", }, rose: { bg: "from-rose-100 to-rose-50/60 dark:from-rose-500/25 dark:to-rose-500/5", text: "text-rose-700 dark:text-rose-300", ring: "text-rose-500/25 dark:text-rose-300/15", }, teal: { bg: "from-teal-100 to-teal-50/60 dark:from-teal-500/25 dark:to-teal-500/5", text: "text-teal-700 dark:text-teal-300", ring: "text-teal-500/25 dark:text-teal-300/15", }, indigo: { bg: "from-indigo-100 to-indigo-50/60 dark:from-indigo-500/25 dark:to-indigo-500/5", text: "text-indigo-700 dark:text-indigo-300", ring: "text-indigo-500/25 dark:text-indigo-300/15", }, orange: { bg: "from-orange-100 to-orange-50/60 dark:from-orange-500/25 dark:to-orange-500/5", text: "text-orange-700 dark:text-orange-300", ring: "text-orange-500/25 dark:text-orange-300/15", }, }; const order = Object.keys(tones) as TeamTone[]; function initials(name: string) { return name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase(); } function useNow() { const [now, setNow] = useState(null); useEffect(() => { setNow(new Date()); const id = window.setInterval(() => setNow(new Date()), 30_000); return () => window.clearInterval(id); }, []); return now; } function localTime(now: Date | null, timeZone?: string) { if (!now || !timeZone) return null; try { return new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", timeZone }).format(now); } catch { return null; } } function MemberCard({ member, index, now }: { member: TeamMember; index: number; now: Date | null }) { const reduce = useReducedMotionSafe(); const bioId = useId(); const [hover, setHover] = useState(false); const [focus, setFocus] = useState(false); const [pinned, setPinned] = useState(false); const open = hover || focus || pinned; const tone = tones[member.tone ?? order[index % order.length]]; const time = localTime(now, member.timeZone); const links = member.links ?? {}; return ( !e.currentTarget.contains(e.relatedTarget as Node) && setFocus(false)} onFocus={() => setFocus(true)} onPointerEnter={(e) => e.pointerType === "mouse" && setHover(true)} onPointerLeave={() => setHover(false)} transition={{ duration: 0.3, ease }} >

{member.name}

{member.role}

{member.location} {time && ( <> {time} )}

{member.name}

{member.role}

{member.bio}

{links.email && ( )} {links.website && ( )} {links.handle && ( {links.handle} )}
); } export function TeamGrid({ eyebrow = "Team", title = "The people behind it", description, members, hiring, className, }: TeamGridProps) { const reduce = useReducedMotionSafe(); const tabsId = useId(); const now = useNow(); const teams = ["Everyone", ...Array.from(new Set(members.map((m) => m.team)))]; const [team, setTeam] = useState("Everyone"); const shown = members.filter((m) => team === "Everyone" || m.team === team); return (
{eyebrow &&

{eyebrow}

}

{title}

{description &&

{description}

}
{teams.map((t) => { const on = t === team; return ( ); })}
); }