"use client"; import { useId, useRef, useState, type CSSProperties, type ElementType, type PointerEvent } from "react"; import { AnimatePresence, motion, useMotionValue, useReducedMotion, useSpring } from "motion/react"; export type PortfolioGridItem = { title: string; /** Used for the filter chips. */ tag: string; src: string; alt: string; href?: string; /** Small line after the tag, such as the year. */ note?: string; /** Takes two columns on a wide screen. */ wide?: boolean; }; type PortfolioGridProps = { items: PortfolioGridItem[]; title?: string; /** Small line above the title. */ eyebrow?: string; subtitle?: string; /** Word for the chip that shows everything. */ allLabel?: string; /** Word on the pill that follows the pointer over a link. */ viewLabel?: string; /** Colour of the active chip and the pill. */ color?: string; /** Text colour on that colour. */ ink?: string; /** h2 by default. */ headingAs?: ElementType; /** Set padding and colours here. */ className?: string; }; const SPRING = { stiffness: 320, damping: 30 }; /* PortfolioGrid: project tiles with filter chips. Tiles glide to their new places when the filter changes; on hover the picture grows, the caption rises and a small pill follows the pointer. */ export function PortfolioGrid({ items, title, eyebrow, subtitle, allLabel = "All", viewLabel = "View", color = "#a78bfa", ink = "#0b0a14", headingAs: Heading = "h2", className = "bg-[#0b0a14] px-5 py-16 text-white", }: PortfolioGridProps) { const reduce = useReducedMotion(); const id = useId(); const grid = useRef(null); const [tag, setTag] = useState(allLabel); const [pill, setPill] = useState(false); const x = useMotionValue(0); const y = useMotionValue(0); const pillX = useSpring(x, SPRING); const pillY = useSpring(y, SPRING); const tags = [allLabel, ...new Set(items.map((item) => item.tag))]; const shown = items.filter((item) => tag === allLabel || item.tag === tag); const move = (e: PointerEvent) => { const box = grid.current?.getBoundingClientRect(); if (e.pointerType !== "mouse" || !box) return; const over = Boolean((e.target as Element).closest("a")); x.set(e.clientX - box.left); y.set(e.clientY - box.top); if (over && !pill) { pillX.jump(e.clientX - box.left); pillY.jump(e.clientY - box.top); } setPill(over); }; return (
{title && (
{eyebrow &&

{eyebrow}

} {title} {subtitle &&

{subtitle}

}
)}
{tags.map((name) => { const on = name === tag; return ( ); })}

{shown.length} shown

); }