hirael
blockportfoliosource: ok · refreshed
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.
npx shadcn add "https://hirael.com/r/portfolio.json"free
Where it goes
herofooterstatscontactgallerybento-grid
Visual style
bentodark-futuristic
Interaction
animated-counterparallax
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationgsapmotion
Registry dependencies
button
Files12 · source cached 10/8/2026, 7:15:11 AM
registry/hirael/bases/radix/templates/portfolio/portfolio.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
// 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
// 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 ’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
// 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
<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
// 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
// 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
// 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
// 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))]">© 2026 Mohammad Shehadeh</span>
</div>
</div>
</footer>
);
};
registry/hirael/bases/radix/templates/portfolio/background-video.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
// 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
// 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
// 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;
}
}
`;