# Team cluster 01 default

A shadcn/ui block from the [@craftui](https://www.craftui.space) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/craftui/team-cluster-01-default).

- Page, with a live preview: https://shadcn.exchange/r/craftui/team-cluster-01-default
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/craftui/team-cluster-01-default-light-f2ef6145d396348b.png
- Registry item: https://www.craftui.space/r/team-cluster-01-default.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.craftui.space/r/team-cluster-01-default.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@hugeicons/core-free-icons`, `@hugeicons/react`, `motion`
- Registry items: `https://www.craftui.space/r/craftui-base.json`, `https://www.craftui.space/r/avatar-default.json`

## Files

### `components/craftui/sections/team-cluster-01/default.tsx`

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
  Compass01Icon,
  PaintBoardIcon,
  SourceCodeIcon,
  CustomerSupportIcon,
  Rocket01Icon,
  DocumentValidationIcon,
} from "@hugeicons/core-free-icons";
import AvatarGrid, {
  type AvatarMember,
} from "@/components/craftui/blocks/avatar/default";

type HugeIcon = React.ComponentProps<typeof HugeiconsIcon>["icon"];

export type TeamMember = AvatarMember & {
  icon?: HugeIcon;
  bio?: string;
  links?: { label: string; href: string }[];
};

export interface TeamCluster01Props {
  eyebrow?: string;
  heading?: React.ReactNode;
  body?: React.ReactNode;
  members?: TeamMember[];
  className?: string;
}

const TIMING = {
  heading: 0,
  body: 0.12,
  detail: 0.48,
};

const EASE = [0.165, 0.84, 0.44, 1] as const;
const DURATION = 0.25;

const DEFAULT_MEMBERS: TeamMember[] = [
  {
    id: "maya",
    name: "Maya Ellis",
    role: "Founder & CEO",
    icon: Compass01Icon,
    src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&auto=format&fit=crop&q=80",
    cell: [2, 2],
    bio: "Started the studio in 2019.",
    links: [{ label: "LinkedIn", href: "#" }],
  },
  {
    id: "noor",
    name: "Noor Haddad",
    role: "Head of Design",
    icon: PaintBoardIcon,
    src: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&auto=format&fit=crop&q=80",
    cell: [2, 3],
    bio: "Sets the craft bar, sketch to pixel.",
    links: [{ label: "Portfolio", href: "#" }],
  },
  {
    id: "tom",
    name: "Tom Brandt",
    role: "Principal Engineer",
    icon: SourceCodeIcon,
    src: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&auto=format&fit=crop&q=80",
    cell: [3, 2],
    bio: "Owns the rendering pipeline.",
    links: [{ label: "GitHub", href: "#" }],
  },
  {
    id: "amir",
    name: "Amir Rahal",
    role: "Support Lead",
    icon: CustomerSupportIcon,
    src: "https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=400&auto=format&fit=crop&q=80",
    cell: [3, 4],
    bio: "Answers every ticket personally.",
    links: [{ label: "Email", href: "#" }],
  },
  {
    id: "dev",
    name: "Dev Sharma",
    role: "Product Engineer",
    icon: Rocket01Icon,
    src: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&auto=format&fit=crop&q=80",
    cell: [4, 3],
    bio: "Builds the parts customers touch most.",
    links: [{ label: "GitHub", href: "#" }],
  },
  {
    id: "lena",
    name: "Lena Fischer",
    role: "Content Reviewer",
    icon: DocumentValidationIcon,
    src: "https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=400&auto=format&fit=crop&q=80",
    cell: [4, 4],
    bio: "Keeps the documentation honest.",
    links: [{ label: "Writing", href: "#" }],
  },
];

export default function TeamCluster01({
  eyebrow = "Know my team",
  heading = "The people behind the work",
  body = "Six of us, one studio, and a shared refusal to ship things that feel almost right.",
  members = DEFAULT_MEMBERS,
  className,
}: TeamCluster01Props = {}) {
  const scopeId = React.useId().replace(/:/g, "");
  const reduceMotion = useReducedMotion();
  const [selectedId, setSelectedId] = React.useState<string | null>(
    members[0]?.id ?? null,
  );
  const [activeId, setActiveId] = React.useState<string | null>(
    members[0]?.id ?? null,
  );
  const rise = (delay: number) => ({
    initial: reduceMotion ? { opacity: 0 } : { opacity: 0, y: 12 },
    whileInView: reduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 },
    viewport: { once: true, amount: 0.3 },
    transition: { duration: DURATION, ease: EASE, delay },
  });

  return (
    <section
      className={`w-full bg-[var(--team-surface)] py-16 sm:py-20 xl:py-28 ${className ?? ""}`}
    >
      <style>
        {`
          .team-${scopeId} {
            display: grid;
            gap: 2.5rem;
            grid-template-areas: "copy" "cluster" "detail";
          }
          @media (min-width: 1280px) {
            .team-${scopeId} {
              grid-template-columns: minmax(0, 1fr) minmax(0, 550px);
              grid-template-areas: "copy cluster" "detail cluster";
              align-items: end;
              column-gap: 1rem;
              row-gap: 2rem;
            }
            .team-${scopeId} [data-role-badge] {
              display: none;
            }
          }
        `}
      </style>

      <div
        className={`team-${scopeId} mx-auto max-w-6xl px-5 sm:px-8 xl:max-w-6xl`}
      >
        <div
          style={{ gridArea: "copy" }}
          className="mx-auto max-w-xl text-left md:text-center xl:mx-0 xl:max-w-none xl:text-left"
        >
          <motion.p
            {...rise(TIMING.heading)}
            className="text-xs font-medium tracking-[0.14em] text-[var(--team-fg-muted)] uppercase"
          >
            {eyebrow}
          </motion.p>
          <motion.h2
            {...rise(TIMING.heading)}
            className="mt-3 text-3xl font-medium tracking-tight text-balance text-[var(--team-fg)] sm:text-4xl xl:text-[2.75rem] xl:leading-[1.1]"
          >
            {heading}
          </motion.h2>
          <motion.p
            {...rise(TIMING.body)}
            className="mx-auto mt-4 max-w-md text-base leading-relaxed text-pretty text-[var(--team-fg-muted)] xl:mx-0"
          >
            {body}
          </motion.p>
        </div>

        <div style={{ gridArea: "cluster" }} className="xl:-ml-[94px]">
          <AvatarGrid
            members={members}
            selectedId={selectedId}
            onSelect={setSelectedId}
            onActiveChange={setActiveId}
            roleBadges
            renderBadge={(member) => (
              <span className="block text-left">
                <span className="block">{member.name}</span>
                <span className="mt-0.5 block text-[9px] font-normal opacity-75">
                  {member.role}
                </span>
              </span>
            )}
            className="!p-0 !justify-start xl:[&>div]:max-w-[600px]"
          />
        </div>

        <motion.div
          style={{ gridArea: "detail" }}
          {...rise(TIMING.detail)}
          className="hidden md:block"
        >
          <ul className="mx-auto max-w-md text-center [--hatch-ink:oklch(0_0_0_/_0.12)] xl:mx-0 xl:text-left dark:[--hatch-ink:oklch(1_0_0_/_0.16)]">
            {members.map((member, index) => {
              const isActive = activeId === member.id;
              return (
                <li key={member.id}>
                  {index > 0 && (
                    <div
                      aria-hidden="true"
                      className="h-1"
                      style={{
                        backgroundImage:
                          "repeating-linear-gradient(45deg, var(--hatch-ink) 0 1px, transparent 1px 6px)",
                      }}
                    />
                  )}
                  <button
                    type="button"
                    aria-pressed={isActive}
                    aria-label={`${member.name} — ${member.role}`}
                    onClick={() => setSelectedId(member.id)}
                    onPointerEnter={(event) => {
                      if (event.pointerType === "mouse")
                        setSelectedId(member.id);
                    }}
                    className={`flex w-full cursor-pointer items-start justify-between gap-6 py-2.5 text-sm transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--team-ring)] ${
                      isActive
                        ? "font-medium text-[var(--team-fg)]"
                        : "text-[var(--team-fg-muted)] hover:text-[var(--team-fg)]"
                    }`}
                  >
                    <span className="flex min-w-0 shrink-0 items-center gap-2.5 text-left">
                      {member.icon && (
                        <HugeiconsIcon
                          icon={member.icon}
                          size={16}
                          strokeWidth={1.5}
                          color="currentColor"
                          className="shrink-0"
                          aria-hidden="true"
                        />
                      )}
                      {member.role}
                    </span>
                    <span className="flex min-w-0 flex-col items-end text-right">
                      <span
                        className={`text-xs font-normal transition-colors duration-150 ease-out ${
                          isActive
                            ? "text-[var(--team-fg)]"
                            : "text-[var(--team-fg-soft)]"
                        }`}
                      >
                        {member.name}
                      </span>
                      {member.bio && (
                        <span className="mt-0.5 line-clamp-1 text-xs leading-snug font-normal text-[var(--team-fg-muted)]">
                          {member.bio}
                        </span>
                      )}
                    </span>
                  </button>
                </li>
              );
            })}
          </ul>
        </motion.div>
      </div>
    </section>
  );
}
```
