# Portfolio

> Dark, single-page personal portfolio: a counter loading screen, an HLS video hero with a floating nav and a cycling role line, a bento work grid, a journal list, a scroll-pinned parallax gallery with lightbox, count-up stats and a video contact footer. Self-contained dark palette, driven by GSAP and Framer Motion.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/portfolio).

- Page, with a live preview: https://shadcn.exchange/r/hirael/portfolio
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/portfolio-light-4ac0debbd690b030.png
- Registry item: https://hirael.com/r/portfolio.json
- Categories: templates
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/portfolio.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: `gsap`, `motion`
- Registry items: `button`

## Files

### `registry/hirael/bases/radix/templates/portfolio/portfolio.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

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

import { cn } from '@/lib/utils';

import { Contact } from './contact';
import { Explorations } from './explorations';
import { inter, instrumentSerif } from './fonts';
import { Hero } from './hero';
import { Journal } from './journal';
import { LoadingScreen } from './loading-screen';
import { SelectedWorks } from './selected-works';
import { Stats } from './stats';
import { PORTFOLIO_STYLES } from './styles';

const Portfolio = () => {
  const [isLoading, setIsLoading] = React.useState(true);

  return (
    <div
      data-slot="portfolio"
      className={cn(
        inter.variable,
        instrumentSerif.variable,
        'min-h-svh bg-[hsl(var(--bg))] text-[hsl(var(--text))] antialiased',
      )}
    >
      <style dangerouslySetInnerHTML={{ __html: PORTFOLIO_STYLES }} />

      <MotionConfig reducedMotion="user">
        {isLoading ? <LoadingScreen onComplete={() => setIsLoading(false)} /> : null}

        <Hero start={!isLoading} />
        <SelectedWorks />
        <Journal />
        <Explorations />
        <Stats />
        <Contact />
      </MotionConfig>
    </div>
  );
};

export default Portfolio;
```

### `registry/hirael/bases/radix/templates/portfolio/loading-screen.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

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

const WORDS = ['Design', 'Create', 'Inspire'];
const DURATION = 2700;
const WORD_INTERVAL = 900;
const EXIT_DELAY = 400;

interface LoadingScreenProps {
  onComplete: () => void;
}

export const LoadingScreen = ({ onComplete }: LoadingScreenProps) => {
  const [count, setCount] = React.useState(0);
  const [wordIndex, setWordIndex] = React.useState(0);
  const [exiting, setExiting] = React.useState(false);

  const onCompleteRef = React.useRef(onComplete);
  React.useEffect(() => {
    onCompleteRef.current = onComplete;
  });

  React.useEffect(() => {
    let raf = 0;
    let exitTimer: ReturnType<typeof setTimeout> | undefined;
    const start = performance.now();

    const tick = (now: number) => {
      const progress = Math.min((now - start) / DURATION, 1);
      setCount(Math.round(progress * 100));
      if (progress < 1) {
        raf = requestAnimationFrame(tick);

        return;
      }
      setExiting(true);
      exitTimer = setTimeout(() => onCompleteRef.current(), EXIT_DELAY);
    };

    raf = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(raf);
      if (exitTimer) clearTimeout(exitTimer);
    };
  }, []);

  React.useEffect(() => {
    const id = setInterval(() => setWordIndex((i) => (i + 1) % WORDS.length), WORD_INTERVAL);

    return () => clearInterval(id);
  }, []);

  return (
    <motion.div
      aria-hidden="true"
      initial={{ opacity: 1 }}
      animate={{ opacity: exiting ? 0 : 1 }}
      transition={{ duration: EXIT_DELAY / 1000, ease: 'easeInOut' }}
      className="fixed inset-0 z-[9999] overflow-hidden bg-[hsl(var(--bg))] text-[hsl(var(--text))]"
    >
      <motion.span
        initial={{ y: -20, opacity: 0 }}
        animate={{ y: 0, opacity: 1 }}
        transition={{ duration: 0.6, ease: 'easeOut' }}
        className="absolute start-6 top-6 text-xs tracking-[0.3em] text-[hsl(var(--muted))] uppercase sm:start-10 sm:top-10"
      >
        Portfolio
      </motion.span>

      <div className="absolute inset-0 flex items-center justify-center">
        <AnimatePresence mode="wait">
          <motion.span
            key={wordIndex}
            initial={{ y: 20, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            exit={{ y: -20, opacity: 0 }}
            transition={{ duration: 0.4, ease: 'easeOut' }}
            className="font-display text-4xl text-[hsl(var(--text))]/80 italic md:text-6xl lg:text-7xl"
          >
            {WORDS[wordIndex]}
          </motion.span>
        </AnimatePresence>
      </div>

      <span className="font-display absolute end-6 bottom-6 text-6xl tabular-nums md:end-10 md:bottom-10 md:text-8xl lg:text-9xl">
        {String(count).padStart(3, '0')}
      </span>

      <div className="absolute inset-x-0 bottom-0 h-[3px] bg-[hsl(var(--stroke))]/50">
        <div
          className="pf-gradient h-full shadow-[0_0_8px_color-mix(in_oklch,var(--primary)_35%,transparent)]"
          style={{ width: `${count}%` }}
        />
      </div>
    </motion.div>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/hero.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import gsap from 'gsap';

import { cn } from '@/lib/utils';

import { BACKGROUND_VIDEO, BackgroundVideo } from './background-video';
import { ArrowUpRight, onAnchorClick, RingLink } from './primitives';

const NAV_LINKS = [
  { label: 'Home', href: '#home', id: 'home' },
  { label: 'Work', href: '#work', id: 'work' },
  { label: 'Resume', href: '#stats', id: 'stats' },
];
const ROLES = ['Creative', 'Developer', 'Founder', 'Scholar'];
const ROLE_INTERVAL = 2000;

const Logo = () => {
  return (
    <a
      href="#home"
      aria-label="Home"
      onClick={(e) => onAnchorClick(e, '#home')}
      className="group relative flex size-9 shrink-0 items-center justify-center rounded-full transition-transform duration-300 hover:scale-110"
    >
      <span className="pf-gradient absolute inset-0 rounded-full" />
      <span className="absolute inset-0 rounded-full bg-[linear-gradient(270deg,color-mix(in_oklch,var(--primary)_55%,var(--background))_0%,var(--primary)_100%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
      <span className="absolute inset-[1.5px] rounded-full bg-[hsl(var(--bg))]" />
      <span className="font-display relative text-[13px] leading-none italic">MS</span>
    </a>
  );
};

const Navbar = () => {
  const [scrolled, setScrolled] = React.useState(false);
  const [active, setActive] = React.useState('home');

  React.useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 100);
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });

    return () => window.removeEventListener('scroll', onScroll);
  }, []);

  React.useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) setActive(entry.target.id);
        }
      },
      { rootMargin: '-45% 0px -50% 0px', threshold: 0 },
    );
    for (const link of NAV_LINKS) {
      const el = document.getElementById(link.id);
      if (el) observer.observe(el);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <nav className="fixed inset-x-0 top-0 z-50 flex justify-center px-4 pt-4 md:pt-6">
      <div
        className={cn(
          'inline-flex items-center rounded-full border border-white/10 bg-[hsl(var(--surface))] px-2 py-2 backdrop-blur-md transition-shadow duration-300',
          scrolled && 'shadow-md shadow-black/10',
        )}
      >
        <Logo />
        <span className="mx-1 hidden h-5 w-px bg-[hsl(var(--stroke))] sm:block" />
        <div className="flex items-center gap-0.5">
          {NAV_LINKS.map((link) => {
            const isActive = active === link.id;

            return (
              <a
                key={link.label}
                href={link.href}
                onClick={(e) => onAnchorClick(e, link.href)}
                aria-current={isActive ? 'page' : undefined}
                className={cn(
                  'rounded-full px-3 py-1.5 text-xs transition-colors duration-200 sm:px-4 sm:py-2 sm:text-sm',
                  isActive
                    ? 'bg-[hsl(var(--stroke))]/50 text-[hsl(var(--text))]'
                    : 'text-[hsl(var(--muted))] hover:bg-[hsl(var(--stroke))]/50 hover:text-[hsl(var(--text))]',
                )}
              >
                {link.label}
              </a>
            );
          })}
        </div>
        <span className="mx-1 hidden h-5 w-px bg-[hsl(var(--stroke))] sm:block" />
        <RingLink
          href="#contact"
          innerClassName="bg-[hsl(var(--surface))] px-3 py-1.5 text-xs text-[hsl(var(--text))] backdrop-blur-md sm:px-4 sm:py-2 sm:text-sm"
        >
          Say hi
          <ArrowUpRight className="size-3 rtl:-scale-x-100" />
        </RingLink>
      </div>
    </nav>
  );
};

interface HeroProps {
  start?: boolean;
}

export const Hero = ({ start = true }: HeroProps) => {
  const rootRef = React.useRef<HTMLElement>(null);
  const [roleIndex, setRoleIndex] = React.useState(0);

  React.useEffect(() => {
    const id = setInterval(() => setRoleIndex((i) => (i + 1) % ROLES.length), ROLE_INTERVAL);

    return () => clearInterval(id);
  }, []);

  React.useEffect(() => {
    if (!start) return;
    const ctx = gsap.context(() => {
      const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (reduce) {
        gsap.set(['.name-reveal', '.lead-rise', '.blur-in'], {
          opacity: 1,
          y: 0,
          filter: 'blur(0px)',
        });

        return;
      }
      // The name and lead only slide: text that starts at opacity 0 holds back Largest Contentful Paint.
      const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
      tl.fromTo('.name-reveal', { y: 50 }, { y: 0, duration: 1.2, delay: 0.1 })
        .fromTo('.lead-rise', { y: 20 }, { y: 0, duration: 1, stagger: 0.1 }, 0.3)
        .fromTo(
          '.blur-in',
          { opacity: 0, y: 20, filter: 'blur(10px)' },
          {
            opacity: 1,
            y: 0,
            filter: 'blur(0px)',
            duration: 1,
            stagger: 0.1,
          },
          0.3,
        );
    }, rootRef);

    return () => ctx.revert();
  }, [start]);

  return (
    <section
      ref={rootRef}
      id="home"
      className="relative flex h-dvh w-full flex-col items-center justify-center overflow-hidden"
    >
      <Navbar />

      <div className="absolute inset-0 -z-10">
        <BackgroundVideo
          src={BACKGROUND_VIDEO}
          className="absolute top-1/2 left-1/2 size-full min-h-full min-w-full -translate-x-1/2 -translate-y-1/2 object-cover"
        />
        <div className="absolute inset-0 bg-black/20" />
        <div className="absolute inset-x-0 bottom-0 h-48 bg-gradient-to-t from-[hsl(var(--bg))] to-transparent" />
      </div>

      <div className="relative z-10 flex flex-col items-center px-6 text-center">
        <span className="mb-8 text-xs tracking-[0.3em] text-[hsl(var(--muted))] uppercase opacity-0 blur-in">
          Collection &rsquo;26
        </span>
        <h1 className="name-reveal font-display mb-6 [transform:translateY(50px)] text-6xl leading-[0.9] tracking-tight italic motion-reduce:[transform:none] md:text-8xl lg:text-9xl">
          Mohammad Shehadeh
        </h1>
        <p className="lead-rise mb-3 [transform:translateY(20px)] text-lg text-[hsl(var(--text))] motion-reduce:[transform:none] sm:text-xl">
          A{' '}
          <span
            key={roleIndex}
            className="animate-role-fade-in font-display inline-block text-[hsl(var(--text))] italic"
          >
            {ROLES[roleIndex]}
          </span>{' '}
          lives in Chicago.
        </p>
        <p className="lead-rise mb-12 max-w-md [transform:translateY(20px)] text-sm text-[hsl(var(--muted))] motion-reduce:[transform:none] md:text-base">
          Designing seamless digital interactions by focusing on the unique nuances which bring systems to life.
        </p>
        <div className="inline-flex flex-wrap items-center justify-center gap-4 opacity-0 blur-in">
          <RingLink
            href="#work"
            outerClassName="transition-transform duration-300 hover:scale-105"
            innerClassName="bg-[hsl(var(--text))] px-7 py-3.5 text-sm text-[hsl(var(--bg))] group-hover:bg-[hsl(var(--bg))] group-hover:text-[hsl(var(--text))]"
          >
            See Works
          </RingLink>
          <RingLink
            href="#contact"
            outerClassName="transition-transform duration-300 hover:scale-105"
            innerClassName="border-2 border-[hsl(var(--stroke))] bg-[hsl(var(--bg))] px-7 py-3.5 text-sm text-[hsl(var(--text))] group-hover:border-transparent"
          >
            Reach out
          </RingLink>
        </div>
      </div>

      <div className="absolute bottom-8 left-1/2 z-10 flex -translate-x-1/2 flex-col items-center gap-3">
        <span className="text-xs tracking-[0.2em] text-[hsl(var(--muted))] uppercase">Scroll</span>
        <span className="relative block h-10 w-px overflow-hidden bg-[hsl(var(--stroke))]">
          <span className="pf-gradient animate-scroll-down absolute inset-x-0 top-0 block h-1/2 w-full" />
        </span>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/selected-works.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import { motion } from 'motion/react';

import { cn } from '@/lib/utils';

import { onAnchorClick, SectionHeader } from './primitives';

const PROJECTS = [
  {
    title: 'Automotive Motion',
    image: 'https://hirael.com/media/templates/portfolio/work-automotive-motion.jpg',
    span: 'md:col-span-7',
  },
  {
    title: 'Urban Architecture',
    image: 'https://hirael.com/media/templates/portfolio/work-urban-architecture.jpg',
    span: 'md:col-span-5',
  },
  {
    title: 'Human Perspective',
    image: 'https://hirael.com/media/templates/portfolio/work-human-perspective.jpg',
    span: 'md:col-span-5',
  },
  {
    title: 'Brand Identity',
    image: 'https://hirael.com/media/templates/portfolio/work-brand-identity.jpg',
    span: 'md:col-span-7',
  },
];

const HALFTONE = {
  backgroundImage: 'radial-gradient(circle, #000 1px, transparent 1px)',
  backgroundSize: '4px 4px',
};

export const SelectedWorks = () => {
  return (
    <section id="work" className="bg-[hsl(var(--bg))] py-12 md:py-16">
      <div className="mx-auto max-w-[1200px] px-6 md:px-10 lg:px-16">
        <SectionHeader
          eyebrow="Selected Work"
          lead="Featured"
          accent="projects"
          subtext="A selection of projects I've worked on, from concept to launch."
          viewAll={{ label: 'View all work', href: '#work' }}
          className="mb-10 md:mb-14"
        />

        <motion.div
          initial="hidden"
          whileInView="show"
          viewport={{ once: true, margin: '-80px' }}
          variants={{
            hidden: {},
            show: { transition: { staggerChildren: 0.12 } },
          }}
          className="grid grid-cols-1 gap-5 md:grid-cols-12 md:gap-6"
        >
          {PROJECTS.map((project) => (
            <motion.a
              key={project.title}
              href="#work"
              onClick={(e) => onAnchorClick(e, '#work')}
              variants={{
                hidden: { opacity: 0, y: 30 },
                show: { opacity: 1, y: 0 },
              }}
              transition={{ duration: 0.7, ease: [0.25, 0.1, 0.25, 1] }}
              className={cn(
                'group relative block h-[320px] overflow-hidden rounded-3xl border border-[hsl(var(--stroke))] bg-[hsl(var(--surface))] md:h-[440px]',
                project.span,
              )}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={project.image}
                alt={project.title}
                loading="lazy"
                className="absolute inset-0 size-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
              />
              <span aria-hidden="true" className="absolute inset-0 opacity-20 mix-blend-multiply" style={HALFTONE} />
              <span className="absolute inset-0 flex items-center justify-center bg-[hsl(var(--bg))]/70 opacity-0 backdrop-blur-lg transition-opacity duration-500 group-hover:opacity-100">
                <span className="pf-gradient-animated rounded-full p-px">
                  <span className="flex items-center rounded-full bg-white px-4 py-2 text-sm text-black">
                    View&nbsp;
                    <span className="font-display italic">{project.title}</span>
                  </span>
                </span>
              </span>
            </motion.a>
          ))}
        </motion.div>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/journal.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import { motion } from 'motion/react';

import { ArrowUpRight, onAnchorClick, SectionHeader } from './primitives';

const ENTRIES = [
  {
    title: 'Designing for motion and meaning',
    image: 'https://hirael.com/media/templates/portfolio/journal-motion-and-meaning.jpg',
    readTime: '6 min read',
    date: 'May 2026',
  },
  {
    title: 'The quiet power of constraints',
    image: 'https://hirael.com/media/templates/portfolio/journal-quiet-constraints.jpg',
    readTime: '4 min read',
    date: 'Apr 2026',
  },
  {
    title: 'Systems that scale with the team',
    image: 'https://hirael.com/media/templates/portfolio/journal-systems-that-scale.jpg',
    readTime: '8 min read',
    date: 'Mar 2026',
  },
  {
    title: 'Notes on color and contrast',
    image: 'https://hirael.com/media/templates/portfolio/journal-color-and-contrast.jpg',
    readTime: '5 min read',
    date: 'Feb 2026',
  },
];

export const Journal = () => {
  return (
    <section id="journal" className="bg-[hsl(var(--bg))] py-16 md:py-24">
      <div className="mx-auto max-w-[1200px] px-6 md:px-10 lg:px-16">
        <SectionHeader
          eyebrow="Journal"
          lead="Recent"
          accent="thoughts"
          subtext="Essays and notes on craft, process, and the work behind the work."
          viewAll={{ label: 'View all', href: '#journal' }}
          className="mb-10 md:mb-14"
        />

        <div className="flex flex-col gap-4">
          {ENTRIES.map((entry, i) => (
            <motion.a
              key={entry.title}
              href="#journal"
              onClick={(e) => onAnchorClick(e, '#journal')}
              initial={{ opacity: 0, y: 24 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true, margin: '-60px' }}
              transition={{
                duration: 0.6,
                delay: i * 0.08,
                ease: [0.25, 0.1, 0.25, 1],
              }}
              className="group flex items-center gap-4 rounded-[40px] border border-[hsl(var(--stroke))] bg-[hsl(var(--surface))]/30 p-4 transition-colors duration-300 hover:bg-[hsl(var(--surface))] sm:gap-6 sm:rounded-full"
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={entry.image}
                alt=""
                loading="lazy"
                className="size-16 shrink-0 rounded-full object-cover sm:size-20"
              />
              <div className="min-w-0 flex-1">
                <h3 className="truncate text-base font-medium sm:text-lg">{entry.title}</h3>
                <p className="mt-1 text-xs text-[hsl(var(--muted))] sm:text-sm">
                  {entry.readTime}, {entry.date}
                </p>
              </div>
              <span className="me-2 flex size-9 shrink-0 items-center justify-center rounded-full border border-[hsl(var(--stroke))] text-[hsl(var(--muted))] transition-colors duration-300 group-hover:text-[hsl(var(--text))]">
                <ArrowUpRight className="size-4 rtl:-scale-x-100" />
              </span>
            </motion.a>
          ))}
        </div>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/explorations.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { AnimatePresence, motion } from 'motion/react';

import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';

import { ArrowUpRight, RingLink } from './primitives';

const ITEMS = [
  {
    image: 'https://hirael.com/media/templates/portfolio/exploration-1.jpg',
    rotate: -4,
  },
  {
    image: 'https://hirael.com/media/templates/portfolio/exploration-2.jpg',
    rotate: 3,
  },
  {
    image: 'https://hirael.com/media/templates/portfolio/exploration-3.jpg',
    rotate: -2,
  },
  {
    image: 'https://hirael.com/media/templates/portfolio/exploration-4.jpg',
    rotate: 4,
  },
  {
    image: 'https://hirael.com/media/templates/portfolio/exploration-5.jpg',
    rotate: -3,
  },
  {
    image: 'https://hirael.com/media/templates/portfolio/exploration-6.jpg',
    rotate: 2,
  },
];

interface CardProps {
  image: string;
  rotate: number;
  onOpen: () => void;
}

const Card = ({ image, rotate, onOpen }: CardProps) => {
  return (
    <button
      type="button"
      onClick={onOpen}
      style={{ rotate: `${rotate}deg` }}
      className="group block w-full max-w-[320px] cursor-pointer"
    >
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={image}
        alt="Exploration"
        loading="lazy"
        className="aspect-square w-full rounded-2xl border border-[hsl(var(--stroke))] object-cover transition-transform duration-500 ease-out group-hover:scale-[1.03]"
      />
    </button>
  );
};

export const Explorations = () => {
  const sectionRef = React.useRef<HTMLDivElement>(null);
  const pinRef = React.useRef<HTMLDivElement>(null);
  const col1Ref = React.useRef<HTMLDivElement>(null);
  const col2Ref = React.useRef<HTMLDivElement>(null);
  const [active, setActive] = React.useState<string | null>(null);

  React.useEffect(() => {
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) return;

    gsap.registerPlugin(ScrollTrigger);
    const ctx = gsap.context(() => {
      ScrollTrigger.create({
        trigger: sectionRef.current,
        start: 'top top',
        end: 'bottom bottom',
        pin: pinRef.current,
        pinSpacing: false,
      });
      const scrub = (yPercent: number) => ({
        yPercent,
        ease: 'none' as const,
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top top',
          end: 'bottom bottom',
          scrub: 1,
        },
      });
      gsap.to(col1Ref.current, scrub(-10));
      gsap.to(col2Ref.current, scrub(-26));
    }, sectionRef);

    return () => ctx.revert();
  }, []);

  React.useEffect(() => {
    if (!active) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setActive(null);
    };
    window.addEventListener('keydown', onKey);

    return () => window.removeEventListener('keydown', onKey);
  }, [active]);

  return (
    <section ref={sectionRef} id="explorations" className="relative min-h-[300vh] overflow-clip bg-[hsl(var(--bg))]">
      <div ref={pinRef} className="pointer-events-none relative z-10 flex h-svh items-center justify-center">
        <div className="px-6 text-center">
          <div className="flex items-center justify-center gap-3">
            <span className="h-px w-8 bg-[hsl(var(--stroke))]" />
            <span className="text-xs tracking-[0.3em] text-[hsl(var(--muted))] uppercase">Explorations</span>
            <span className="h-px w-8 bg-[hsl(var(--stroke))]" />
          </div>
          <h2 className="mt-5 text-4xl leading-[1.05] font-medium tracking-tight sm:text-5xl">
            Visual <span className="font-display font-normal italic">playground</span>
          </h2>
          <p className="mx-auto mt-4 max-w-sm text-sm text-[hsl(var(--muted))]">
            Loose experiments, off-brief sketches and the studies that never shipped.
          </p>
          <div className="pointer-events-auto mt-7 flex justify-center">
            <RingLink
              href="https://github.com/mohammadshehadeh"
              target="_blank"
              innerClassName="bg-[hsl(var(--surface))] px-5 py-2.5 text-sm text-[hsl(var(--text))] backdrop-blur-md"
            >
              GitHub
              <ArrowUpRight className="size-3.5 rtl:-scale-x-100" />
            </RingLink>
          </div>
        </div>
      </div>

      <div className="absolute inset-0 z-20">
        <div className="mx-auto grid h-full max-w-[1400px] grid-cols-2 gap-12 px-6 md:gap-40">
          <div ref={col1Ref} className="flex flex-col items-center gap-[28vh] pt-[36vh]">
            {ITEMS.slice(0, 3).map((item) => (
              <Card key={item.image} image={item.image} rotate={item.rotate} onOpen={() => setActive(item.image)} />
            ))}
          </div>
          <div ref={col2Ref} className="flex flex-col items-center gap-[28vh] pt-[64vh]">
            {ITEMS.slice(3, 6).map((item) => (
              <Card key={item.image} image={item.image} rotate={item.rotate} onOpen={() => setActive(item.image)} />
            ))}
          </div>
        </div>
      </div>

      <AnimatePresence>
        {active ? (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.25 }}
            role="dialog"
            aria-modal="true"
            aria-label="Exploration preview"
            onClick={() => setActive(null)}
            className={cn('fixed inset-0 z-[100] flex items-center justify-center bg-black/90 p-6 text-white')}
          >
            <Button
              type="button"
              variant="ghost"
              size="icon-lg"
              aria-label="Close"
              onClick={() => setActive(null)}
              className="absolute end-6 top-6"
            >
              <svg
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="1.8"
                strokeLinecap="round"
                aria-hidden="true"
                className="size-4"
              >
                <path d="M6 6l12 12M18 6 6 18" />
              </svg>
            </Button>
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={active}
              alt="Exploration"
              onClick={(e) => e.stopPropagation()}
              className="max-h-[85vh] max-w-[90vw] rounded-2xl object-contain"
            />
          </motion.div>
        ) : null}
      </AnimatePresence>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/stats.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

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

const STATS = [
  { value: 20, suffix: '+', label: 'Years Experience' },
  { value: 95, suffix: '+', label: 'Projects Done' },
  { value: 200, suffix: '%', label: 'Satisfied Clients' },
];

const useCountUp = (target: number, active: boolean, duration = 1400) => {
  const [value, setValue] = React.useState(0);

  React.useEffect(() => {
    if (!active) return;
    let raf = 0;
    const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      raf = requestAnimationFrame(() => setValue(target));

      return () => cancelAnimationFrame(raf);
    }
    const start = performance.now();
    const tick = (now: number) => {
      const p = Math.min((now - start) / duration, 1);
      const eased = 1 - Math.pow(1 - p, 3);
      setValue(Math.round(eased * target));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    return () => cancelAnimationFrame(raf);
  }, [active, target, duration]);

  return value;
};

interface StatProps {
  value: number;
  suffix: string;
  label: string;
  active: boolean;
}

const Stat = ({ value, suffix, label, active }: StatProps) => {
  const count = useCountUp(value, active);

  return (
    <div className="flex flex-col items-center border-t border-[hsl(var(--stroke))] pt-8 text-center sm:items-start sm:text-start">
      <span className="font-display text-5xl leading-none tabular-nums md:text-7xl">
        {count}
        {suffix}
      </span>
      <span className="mt-4 text-xs tracking-[0.2em] text-[hsl(var(--muted))] uppercase">{label}</span>
    </div>
  );
};

export const Stats = () => {
  const [active, setActive] = React.useState(false);

  return (
    <section id="stats" className="bg-[hsl(var(--bg))] py-16 md:py-24">
      <motion.div
        onViewportEnter={() => setActive(true)}
        viewport={{ once: true, margin: '-80px' }}
        className="mx-auto grid max-w-[1200px] grid-cols-1 gap-10 px-6 sm:grid-cols-3 md:px-10 lg:px-16"
      >
        {STATS.map((stat) => (
          <Stat key={stat.label} active={active} {...stat} />
        ))}
      </motion.div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/contact.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import gsap from 'gsap';

import { cn } from '@/lib/utils';

import { BACKGROUND_VIDEO, BackgroundVideo } from './background-video';
import { ArrowUpRight, RingLink } from './primitives';

const EMAIL = 'hello@mohammadshehadeh.com';

const SOCIALS = [
  {
    label: 'LinkedIn',
    href: 'https://linkedin.com/in/mohammadshhadeh',
    path: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
  },
  {
    label: 'X',
    href: 'https://x.com/_mshehadeh',
    path: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
  },
  {
    label: 'GitHub',
    href: 'https://github.com/mohammadshehadeh',
    path: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.014 2.898-.014 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
  },
];

const Marquee = () => {
  const trackRef = React.useRef<HTMLDivElement>(null);

  React.useEffect(() => {
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) return;
    const ctx = gsap.context(() => {
      gsap.to(trackRef.current, {
        xPercent: -50,
        duration: 40,
        ease: 'none',
        repeat: -1,
      });
    });

    return () => ctx.revert();
  }, []);

  return (
    <div className="flex overflow-hidden" aria-hidden="true">
      <div ref={trackRef} className="flex w-max shrink-0">
        {Array.from({ length: 2 }).map((_, group) => (
          <div key={group} className="flex shrink-0">
            {Array.from({ length: 10 }).map((_, i) => (
              <span
                key={i}
                className="font-display px-6 text-4xl tracking-tight whitespace-nowrap text-[hsl(var(--text))]/90 uppercase italic md:text-6xl"
              >
                Building the future
              </span>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
};

export const Contact = () => {
  return (
    <footer id="contact" className="relative overflow-hidden bg-[hsl(var(--bg))] pt-16 pb-8 md:pt-20 md:pb-12">
      <div className="absolute inset-0 -z-10">
        <BackgroundVideo
          src={BACKGROUND_VIDEO}
          className="absolute top-1/2 left-1/2 size-full min-h-full min-w-full -translate-x-1/2 -translate-y-1/2 -scale-y-100 object-cover"
        />
        <div className="absolute inset-0 bg-black/60" />
        <div className="absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-[hsl(var(--bg))] to-transparent" />
      </div>

      <Marquee />

      <div className="mx-auto max-w-[1200px] px-6 md:px-10 lg:px-16">
        <div className="flex flex-col items-center py-16 text-center md:py-24">
          <span className="mb-6 text-xs tracking-[0.3em] text-[hsl(var(--muted))] uppercase">
            Have a project in mind?
          </span>
          <RingLink
            href={`mailto:${EMAIL}`}
            outerClassName="transition-transform duration-300 hover:scale-105"
            innerClassName="border border-[hsl(var(--stroke))] bg-[hsl(var(--bg))]/70 px-6 py-4 font-display text-2xl italic text-[hsl(var(--text))] backdrop-blur-md group-hover:border-transparent sm:text-3xl md:text-4xl"
          >
            {EMAIL}
            <ArrowUpRight className="size-5 rtl:-scale-x-100" />
          </RingLink>
        </div>

        <div className="flex flex-col gap-6 border-t border-[hsl(var(--stroke))] pt-6 sm:flex-row sm:items-center sm:justify-between">
          <div className="flex items-center gap-2.5">
            <span className="relative flex size-2.5">
              <span className="absolute inline-flex size-full animate-ping rounded-full bg-success/70" />
              <span className="relative inline-flex size-2.5 rounded-full bg-success" />
            </span>
            <span className="text-sm text-[hsl(var(--muted))]">Available for projects</span>
          </div>

          <div className="flex items-center gap-2">
            {SOCIALS.map((social) => (
              <a
                key={social.label}
                href={social.href}
                target="_blank"
                rel="noreferrer"
                aria-label={social.label}
                className={cn(
                  'flex size-9 items-center justify-center rounded-full border border-[hsl(var(--stroke))] text-[hsl(var(--muted))] transition-colors duration-300',
                  'hover:border-white/15 hover:bg-[hsl(var(--surface))] hover:text-[hsl(var(--text))]',
                )}
              >
                <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="size-4">
                  <path d={social.path} />
                </svg>
              </a>
            ))}
          </div>

          <span className="text-xs text-[hsl(var(--muted))]">&copy; 2026 Mohammad Shehadeh</span>
        </div>
      </div>
    </footer>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/background-video.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

interface BackgroundVideoProps {
  src: string;
  className?: string;
}

export const BackgroundVideo = ({ src, className }: BackgroundVideoProps) => (
  <video src={src} autoPlay muted loop playsInline aria-hidden="true" tabIndex={-1} className={className} />
);

export const BACKGROUND_VIDEO = 'https://hirael.com/media/templates/portfolio/background.mp4';
```

### `registry/hirael/bases/radix/templates/portfolio/primitives.tsx`

```tsx
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

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

import { cn } from '@/lib/utils';

const SECTION_EASE: [number, number, number, number] = [0.25, 0.1, 0.25, 1];

export const onAnchorClick = (event: React.MouseEvent<HTMLAnchorElement>, href: string) => {
  if (!href.startsWith('#') || href.length < 2) return;
  const target = document.getElementById(href.slice(1));
  if (!target) return;
  event.preventDefault();
  const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
  target.scrollIntoView({
    behavior: reduce ? 'auto' : 'smooth',
    block: 'start',
  });
};

interface IconProps {
  className?: string;
}

export const ArrowUpRight = ({ className }: IconProps) => {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={cn('size-3.5', className)}
    >
      <path d="M7 17 17 7M9 7h8v8" />
    </svg>
  );
};

export const ArrowRight = ({ className }: IconProps) => {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={cn('size-3.5 rtl:rotate-180', className)}
    >
      <path d="M5 12h14M13 6l6 6-6 6" />
    </svg>
  );
};

interface RingLinkProps {
  href: string;
  children: React.ReactNode;
  ariaLabel?: string;
  target?: string;
  rel?: string;
  outerClassName?: string;
  innerClassName?: string;
}

export const RingLink = ({ href, children, ariaLabel, target, rel, outerClassName, innerClassName }: RingLinkProps) => {
  return (
    <a
      href={href}
      aria-label={ariaLabel}
      target={target}
      rel={rel ?? (target === '_blank' ? 'noreferrer' : undefined)}
      onClick={(e) => onAnchorClick(e, href)}
      className={cn('group relative inline-flex shrink-0', outerClassName)}
    >
      <span
        aria-hidden="true"
        className="pf-gradient pointer-events-none absolute -inset-[2px] rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100"
      />
      <span
        className={cn(
          'relative inline-flex items-center justify-center gap-1.5 rounded-full transition-colors duration-300',
          innerClassName,
        )}
      >
        {children}
      </span>
    </a>
  );
};

interface ViewAllButtonProps {
  label: string;
  href: string;
  className?: string;
}

export const ViewAllButton = ({ label, href, className }: ViewAllButtonProps) => {
  return (
    <RingLink
      href={href}
      outerClassName={cn('hidden md:inline-flex', className)}
      innerClassName="bg-[hsl(var(--surface))] px-4 py-2 text-xs text-[hsl(var(--text))] backdrop-blur-md"
    >
      {label}
      <ArrowRight className="size-3" />
    </RingLink>
  );
};

interface SectionHeaderViewAll {
  label: string;
  href: string;
}

interface SectionHeaderProps {
  eyebrow: string;
  lead: string;
  accent: string;
  trailing?: string;
  subtext: string;
  viewAll?: SectionHeaderViewAll;
  className?: string;
}

export const SectionHeader = ({
  eyebrow,
  lead,
  accent,
  trailing = '',
  subtext,
  viewAll,
  className,
}: SectionHeaderProps) => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 30 }}
      whileInView={{ opacity: 1, y: 0 }}
      transition={{ duration: 1, ease: SECTION_EASE }}
      viewport={{ once: true, margin: '-100px' }}
      className={className}
    >
      <div className="flex items-center gap-3">
        <span className="h-px w-8 bg-[hsl(var(--stroke))]" />
        <span className="text-xs tracking-[0.3em] text-[hsl(var(--muted))] uppercase">{eyebrow}</span>
      </div>
      <div className="mt-5 flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
        <div className="max-w-xl">
          <h2 className="text-3xl leading-[1.05] font-medium tracking-tight sm:text-4xl lg:text-5xl">
            {lead} <span className="font-display font-normal italic">{accent}</span>
            {trailing}
          </h2>
          <p className="mt-4 max-w-md text-sm text-[hsl(var(--muted))] sm:text-base">{subtext}</p>
        </div>
        {viewAll ? <ViewAllButton label={viewAll.label} href={viewAll.href} /> : null}
      </div>
    </motion.div>
  );
};
```

### `registry/hirael/bases/radix/templates/portfolio/fonts.ts`

```ts
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

import { Inter, Instrument_Serif } from 'next/font/google';

export const inter = Inter({
  variable: '--font-inter',
  subsets: ['latin'],
  display: 'swap',
});

export const instrumentSerif = Instrument_Serif({
  variable: '--font-instrument-serif',
  subsets: ['latin', 'latin-ext'],
  weight: '400',
  style: ['normal', 'italic'],
  display: 'swap',
});
```

### `registry/hirael/bases/radix/templates/portfolio/styles.ts`

```ts
// Portfolio from Hirael <https://hirael.com/templates/portfolio>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

// Colors are HSL channels, read in class names as `hsl(var(--token))`.
export const PORTFOLIO_STYLES = `
[data-slot="portfolio"] {
  --bg: 0 0% 4%;
  --surface: 0 0% 8%;
  --text: 0 0% 96%;
  --muted: 0 0% 53%;
  --stroke: 0 0% 12%;
  --accent: 0 0% 96%;
  font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}
[data-slot="portfolio"] .font-display {
  font-family: var(--font-instrument-serif), ui-serif, Georgia, serif;
}
[data-slot="portfolio"] .pf-gradient {
  background-image: linear-gradient(90deg, color-mix(in oklch, var(--primary) 55%, var(--background)) 0%, var(--primary) 100%);
}
[data-slot="portfolio"] .pf-gradient-animated {
  background-image: linear-gradient(90deg, color-mix(in oklch, var(--primary) 55%, var(--background)) 0%, var(--primary) 50%, color-mix(in oklch, var(--primary) 55%, var(--background)) 100%);
  background-size: 200% 100%;
  animation: pf-gradient-shift 6s ease infinite;
}
[data-slot="portfolio"] .animate-scroll-down {
  animation: pf-scroll-down 1.5s ease-in-out infinite;
}
[data-slot="portfolio"] .animate-role-fade-in {
  animation: pf-role-fade-in 0.4s ease-out both;
}
@keyframes pf-scroll-down {
  0% { transform: translateY(-100%); }
  100% { transform: translateY(200%); }
}
@keyframes pf-role-fade-in {
  0% { opacity: 0; transform: translateY(8px); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes pf-gradient-shift {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
@media (prefers-reduced-motion: reduce) {
  [data-slot="portfolio"] .animate-scroll-down,
  [data-slot="portfolio"] .pf-gradient-animated {
    animation: none;
  }
}
`;
```
