A grid of project covers with a filter by discipline; the grid reflows on a spring.
src/registry/eased/templates/agency/work.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { enter, exit, reducedFade, smooth, useSafeReducedMotion } from "../../lib/motion";
import { Segmented } from "../../segmented/segmented";
import { ClientArt } from "./client-art";
import { projects, work, type Discipline, type Project } from "./content";
import { ArrowLink, Section, useHref } from "./kit";
type Filter = "All" | Discipline;
/**
* Selected work: a filter by discipline over a grid of covers, each cover the client's site
* drawn at card size. Filtering fades the leavers out and slides the rest into place on the
* smooth spring; under reduced motion the grid changes at once with a fade.
*/
export function Work() {
const [filter, setFilter] = useState<Filter>("All");
const reduced = useSafeReducedMotion();
const shown = filter === "All" ? projects : projects.filter((p) => p.disciplines.includes(filter));
const options = [work.all, ...work.filters].map((value) => ({ value: value as Filter, label: value }));
return (
<Section id="work" label={work.label} className="flex flex-col gap-pad-lg">
<div className="flex flex-wrap items-center justify-between gap-in">
<div className="max-w-full overflow-x-auto [scrollbar-width:none]">
<Segmented options={options} value={filter} onValueChange={setFilter} aria-label="Filter by discipline" />
</div>
<ArrowLink href={work.more.href}>{work.more.label}</ArrowLink>
</div>
<ul className="grid gap-x-tiles gap-y-pad-lg sm:grid-cols-2 lg:grid-cols-3">
<AnimatePresence initial={false} mode="popLayout">
{shown.map((p) => (
<motion.li
key={p.id}
layout={!reduced}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: reduced ? reducedFade : exit }}
transition={reduced ? reducedFade : { layout: smooth, opacity: enter }}
>
<WorkCard project={p} />
</motion.li>
))}
</AnimatePresence>
</ul>
</Section>
);
}
/** A cover and its caption; the whole card is one link (rule 59), its cover lifting a shade on hover. */
export function WorkCard({ project }: { project: Project }) {
const to = useHref();
return (
<article className="group relative flex flex-col gap-in">
<div className="image-ring relative aspect-[4/3] overflow-hidden rounded-panel">
<div aria-hidden className="absolute inset-0 transition-transform duration-(--eased-duration-fast) ease-enter group-hover-fine:scale-[1.02] reduced:transition-none">
<ClientArt id={project.id} />
</div>
</div>
<div className="flex min-w-0 flex-col gap-1">
<h3 className="text-ui-md font-medium text-text">
<a href={to(`/work#${project.id}`)} className="focus-ring rounded-control-xs after:absolute after:inset-0 after:rounded-panel">
{project.client}
</a>
</h3>
<p className="text-ui text-pretty text-text-secondary">{project.title}</p>
<p className="text-ui-sm text-text-muted">{project.disciplines.join(", ")}</p>
</div>
</article>
);
}