Projects & work
Projects Next
End of a project page: the heading beside the next project's card.
Layout and motion from the Nitip template — see it in contextimport Link from "next/link";
import { ZoomCard } from "@/components/ui/zoom-card";
import { cn } from "@/lib/utils";
/*
* Projects Next — the end of a project page: a divider across the top, the heading on the left and the next
* project's card on the right.
* Source: marcframe/nitip → components/sections/NextProjectSection.tsx (+ design-system ProjectCard).
*
* Layout kept from Framer: heading and card share the row at every width (gap 48 / 64 / 96); the card fills its half
* and stops at 500px from 1024px, aligned right.
* Motion kept: the photo zooms to 105% while the card is hovered, with Framer's 400ms tween (zoom-card). The photo link duplicates the
* title link, so it is hidden from assistive tech and the keyboard.
*/
export type NextProject = {
title: string;
year: string;
href: string;
image: { src: string; alt: string };
services?: { label: string; href?: string }[];
};
export type ProjectsNextProps = {
title?: string;
project?: NextProject;
className?: string;
};
const defaults = {
title: "Next project",
project: {
title: "Pulse Fitness",
year: "2025",
href: "/projects/pulse-fitness",
image: {
src: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?auto=format&fit=crop&w=1000&q=80",
alt: "Rack of dumbbells in a bright gym",
},
services: [
{ label: "Paid advertising", href: "/services/paid-advertising" },
{ label: "SEO", href: "/services/seo" },
],
} as NextProject,
};
export function ProjectsNext({ title = defaults.title, project = defaults.project, className }: ProjectsNextProps) {
return (
<section className={cn("border-t border-border py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) items-start gap-12 px-(--gutter) md:gap-16 lg:gap-24">
<h2 className="min-w-0 flex-1 font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2>
<div className="flex min-w-0 flex-1 flex-col items-end">
<article className="group flex w-full max-w-125 flex-col gap-4">
<Link href={project.href} tabIndex={-1} aria-hidden className="block">
<ZoomCard className="aspect-[545/400]">
<img src={project.image.src} alt={project.image.alt} className="object-cover" />
</ZoomCard>
</Link>
<div className="flex flex-col gap-2">
<div className="flex h-5 items-start justify-between gap-5 overflow-hidden text-sm whitespace-nowrap text-muted-foreground">
<p className="flex min-w-0 flex-wrap gap-x-1">
{project.services?.map((service, i) => {
const label = `${service.label}${i < (project.services?.length ?? 0) - 1 ? "," : ""}`;
return service.href ? (
<Link
key={service.label}
href={service.href}
className="transition-colors hover:text-foreground hover:underline hover:underline-offset-4"
>
{label}
</Link>
) : (
<span key={service.label}>{label}</span>
);
})}
</p>
<p>{project.year}</p>
</div>
<h3 className="font-heading text-h3 font-semibold tracking-tight">
<Link href={project.href} className="hover:underline hover:underline-offset-4">
{project.title}
</Link>
</h3>
</div>
</article>
</div>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/projects-next.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/zoom-card.json2. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json3. Copy the block into
components/projects-next.tsx.import Link from "next/link"; import { ZoomCard } from "@/components/ui/zoom-card"; import { cn } from "@/lib/utils"; /* * Projects Next — the end of a project page: a divider across the top, the heading on the left and the next * project's card on the right. * Source: marcframe/nitip → components/sections/NextProjectSection.tsx (+ design-system ProjectCard). * * Layout kept from Framer: heading and card share the row at every width (gap 48 / 64 / 96); the card fills its half * and stops at 500px from 1024px, aligned right. * Motion kept: the photo zooms to 105% while the card is hovered, with Framer's 400ms tween (zoom-card). The photo link duplicates the * title link, so it is hidden from assistive tech and the keyboard. */ export type NextProject = { title: string; year: string; href: string; image: { src: string; alt: string }; services?: { label: string; href?: string }[]; }; export type ProjectsNextProps = { title?: string; project?: NextProject; className?: string; }; const defaults = { title: "Next project", project: { title: "Pulse Fitness", year: "2025", href: "/projects/pulse-fitness", image: { src: "https://images.unsplash.com/photo-1534438327276-14e5300c3a48?auto=format&fit=crop&w=1000&q=80", alt: "Rack of dumbbells in a bright gym", }, services: [ { label: "Paid advertising", href: "/services/paid-advertising" }, { label: "SEO", href: "/services/seo" }, ], } as NextProject, }; export function ProjectsNext({ title = defaults.title, project = defaults.project, className }: ProjectsNextProps) { return ( <section className={cn("border-t border-border py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) items-start gap-12 px-(--gutter) md:gap-16 lg:gap-24"> <h2 className="min-w-0 flex-1 font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2> <div className="flex min-w-0 flex-1 flex-col items-end"> <article className="group flex w-full max-w-125 flex-col gap-4"> <Link href={project.href} tabIndex={-1} aria-hidden className="block"> <ZoomCard className="aspect-[545/400]"> <img src={project.image.src} alt={project.image.alt} className="object-cover" /> </ZoomCard> </Link> <div className="flex flex-col gap-2"> <div className="flex h-5 items-start justify-between gap-5 overflow-hidden text-sm whitespace-nowrap text-muted-foreground"> <p className="flex min-w-0 flex-wrap gap-x-1"> {project.services?.map((service, i) => { const label = `${service.label}${i < (project.services?.length ?? 0) - 1 ? "," : ""}`; return service.href ? ( <Link key={service.label} href={service.href} className="transition-colors hover:text-foreground hover:underline hover:underline-offset-4" > {label} </Link> ) : ( <span key={service.label}>{label}</span> ); })} </p> <p>{project.year}</p> </div> <h3 className="font-heading text-h3 font-semibold tracking-tight"> <Link href={project.href} className="hover:underline hover:underline-offset-4"> {project.title} </Link> </h3> </div> </article> </div> </div> </section> ); }
Usage
import { ProjectsNext } from "@/components/projects-next";
<ProjectsNext
project={{ title: "Pulse Fitness", year: "2025", href: "/projects/pulse-fitness", image: { src: "/work/pulse.jpg", alt: "…" } }}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | Next project | Heading (h2) on the left. |
| project | { title, year, href, image, services? } | — | The card on the right; its title is an h3 link. |
| className | string | — | Classes for the <section>. |
- A divider runs across the top. Heading and card share the row at every width; the card stops at 500px, aligned right.
- The photo zooms to 105% while the card is hovered (zoom-card).
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS block called ProjectsNext (Next.js). - <section className="border-t border-border py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter), a row (items-start, gap-12 / md:gap-16 / lg:gap-24) at every width. - Left: h2 font-heading text-h2 semibold tracking-tight (flex-1). Right (flex-1, items-end): a project card up to 500px wide — a 545:400 photo that zooms to 1.05 on hover (400ms, cubic-bezier(0.12,0.23,0.5,1)) inside an aria-hidden link, a one-line muted meta row (services, year), an h3 title link. - Only shadcn variables and block tokens.











