Skip to content
6

Team 2

Single-profile experience section: a four-up metric strip, a bio card with a grid-pattern backdrop, and a competencies card that ends in three pause-on-hover marquee rows of skill chips with edge fades. Every element rises in on scroll and respects reduced-motion.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/team-02.json

Code

components/blocks/team-02.tsx
'use client';

import * as React from 'react';
import { motion, useReducedMotion } from 'motion/react';

import { cn } from '@/lib/utils';
import { Badge } from '@/registry/hirael/bases/radix/ui/badge';
import { Marquee } from '@/registry/hirael/bases/radix/components/marquee';

const HEADLINE = 'Ten years of building interfaces that last';

const Title = () => {
  const reduce = useReducedMotion();
  const words = HEADLINE.split(' ');
  const half = Math.floor(words.length / 2);

  return (
    <h2
      data-slot="team-title"
      className="font-serif text-4xl font-medium leading-[1.04] tracking-tight text-balance sm:text-5xl"
    >
      {words.map((word, i) => (
        <motion.span
          key={`${word}-${i}`}
          className={cn('me-[0.25em] inline-block', i < half ? 'text-muted-foreground' : 'text-foreground')}
          initial={reduce ? false : { opacity: 0, y: 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5, ease: 'easeOut', delay: 0.2 + i * 0.08 }}
        >
          {word}
        </motion.span>
      ))}
    </h2>
  );
};

const GridPattern = () => {
  const id = React.useId();

  return (
    <div
      aria-hidden
      data-slot="grid-pattern"
      className="pointer-events-none absolute top-0 start-1/2 -mt-2 -ms-20 h-full w-full [mask-image:linear-gradient(black,transparent)]"
    >
      <div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent [mask-image:radial-gradient(farthest-side_at_top,black,transparent)]">
        <svg className="absolute inset-0 h-full w-full fill-primary/5 stroke-primary/25 mix-blend-overlay">
          <pattern id={id} width="20" height="20" patternUnits="userSpaceOnUse" x="-12" y="4">
            <path d="M.5 20V.5H20" fill="none" />
          </pattern>
          <rect width="100%" height="100%" strokeWidth="0" fill={`url(#${id})`} />
          <svg x="-12" y="4" className="overflow-visible">
            <rect strokeWidth="0" width="21" height="21" x="180" y="20" />
            <rect strokeWidth="0" width="21" height="21" x="200" y="120" />
            <rect strokeWidth="0" width="21" height="21" x="180" y="100" />
            <rect strokeWidth="0" width="21" height="21" x="160" y="120" />
          </svg>
        </svg>
      </div>
    </div>
  );
};

interface Metric {
  label: string;
  value: string;
  subtext: string;
}

const METRICS: readonly Metric[] = [
  {
    label: 'Experience',
    value: '10+ years',
    subtext: 'Product and design systems',
  },
  { label: 'Components', value: '70+ shipped', subtext: 'Across the registry' },
  { label: 'Teams', value: '40 onboarded', subtext: 'From seed to enterprise' },
  { label: 'Locales', value: '12 supported', subtext: 'Including RTL' },
];

const COMPETENCIES = [
  'Design tokens',
  'Accessibility',
  'React 19',
  'Tailwind v4',
  'Motion design',
  'Component APIs',
  'TypeScript',
  'RTL layout',
  'Figma to code',
  'Performance',
  'Documentation',
  'Code review',
] as const;

const Chip = ({ children }: { children: React.ReactNode }) => {
  return (
    <span
      data-slot="team-chip"
      className="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 text-sm font-medium whitespace-nowrap"
    >
      <span aria-hidden className="size-2.5 shrink-0 rounded-full bg-muted-foreground/50" />
      {children}
    </span>
  );
};

const MetricCard = ({ metric, index }: { metric: Metric; index: number }) => {
  const reduce = useReducedMotion();

  return (
    <motion.div
      data-slot="team-metric"
      className="rounded-lg border border-border bg-card p-4 text-center"
      initial={reduce ? false : { opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.5, ease: 'easeOut', delay: index * 0.12 }}
    >
      <h3 className="mb-1 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">{metric.label}</h3>
      <p className="font-serif text-3xl font-medium text-foreground">{metric.value}</p>
      <p className="text-sm text-muted-foreground">{metric.subtext}</p>
    </motion.div>
  );
};

const Team02 = () => {
  const reduce = useReducedMotion();
  const rows = [COMPETENCIES.slice(0, 4), COMPETENCIES.slice(4, 8), COMPETENCIES.slice(8)];

  return (
    <section data-slot="team" className="bg-background py-20 sm:py-28">
      <div className="container w-full">
        <div data-slot="team-header" className="mx-auto flex max-w-2xl flex-col items-center gap-5 text-center">
          <motion.div
            initial={reduce ? false : { opacity: 0, scale: 0.9 }}
            whileInView={{ opacity: 1, scale: 1 }}
            viewport={{ once: true }}
            transition={{ duration: 0.5, ease: 'easeOut' }}
          >
            <Badge
              variant="outline"
              className="rounded-full bg-card/70 px-4 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground backdrop-blur-sm"
            >
              Experience
            </Badge>
          </motion.div>
          <Title />
          <motion.p
            data-slot="team-description"
            className="max-w-2xl text-base text-pretty text-muted-foreground sm:text-lg"
            initial={reduce ? false : { opacity: 0, y: 16 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.5, ease: 'easeOut', delay: 0.4 }}
          >
            The person behind the registry, the numbers that describe the work, and the skills that show up in every
            component.
          </motion.p>
        </div>

        <div data-slot="team-metrics" className="mt-14 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
          {METRICS.map((metric, i) => (
            <MetricCard key={metric.label} metric={metric} index={i} />
          ))}
        </div>

        <div className="mt-8 grid grid-cols-1 gap-8 lg:grid-cols-2 lg:gap-12">
          <motion.article
            data-slot="team-profile"
            className="relative flex h-full flex-col overflow-hidden rounded-lg border border-border bg-card p-6 md:px-10 md:py-8"
            initial={reduce ? false : { opacity: 0, y: -20 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6, ease: 'easeOut' }}
          >
            <GridPattern />
            <motion.div
              className="relative mb-4"
              initial={reduce ? false : { opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.5, ease: 'easeOut', delay: 0.1 }}
            >
              <h3 className="mb-1 text-2xl font-semibold text-foreground md:text-3xl">Layla Haddad</h3>
              <p className="text-muted-foreground">Design systems lead, Hirael</p>
            </motion.div>
            <motion.div
              className="relative flex flex-col gap-3"
              initial={reduce ? false : { opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.5, ease: 'easeOut', delay: 0.2 }}
            >
              <p className="leading-relaxed text-pretty text-muted-foreground">
                Layla spent a decade turning one-off screens into systems that other engineers actually reach for. She
                led the token migration at two product companies before starting Hirael.
              </p>
              <p className="leading-relaxed text-pretty text-muted-foreground">
                She reviews every component before it ships, usually in Arabic first, because if the RTL layout holds,
                the rest tends to follow.
              </p>
            </motion.div>
          </motion.article>

          <div
            data-slot="team-skills"
            className="relative flex h-full flex-col overflow-hidden rounded-lg border border-border bg-card p-6 md:px-10 md:py-8"
          >
            <GridPattern />
            <motion.div
              className="relative mb-4"
              initial={reduce ? false : { opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.5, ease: 'easeOut' }}
            >
              <h3 className="mb-3 text-2xl font-semibold text-foreground md:text-3xl">Core competencies</h3>
              <p className="text-pretty text-muted-foreground">
                The skills that come up in every engagement, from the first token audit to the last accessibility pass.
              </p>
            </motion.div>

            <motion.div
              data-slot="team-marquee"
              className="relative mt-auto flex flex-col gap-2 pt-6 [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]"
              initial={reduce ? false : { opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.5, ease: 'easeOut', delay: 0.2 }}
            >
              {rows.map((row, i) => (
                <Marquee key={i} pauseOnHover reverse={i === 1} duration={30 + i * 5} gap="0.5rem">
                  {row.map((name) => (
                    <Chip key={name}>{name}</Chip>
                  ))}
                </Marquee>
              ))}
            </motion.div>
          </div>
        </div>
      </div>
    </section>
  );
};

export default Team02;

Dependencies

shadcn registry

badgemarquee

npm

motion

Shipped