Listings
Listings Locations
A centered heading above area cards — three, then two a row — whose photos settle on hover.
Layout and motion from the Findit template — see it in contextimport Link from "next/link";
import { ZoomCard } from "@/components/ui/zoom-card";
import { cn } from "@/lib/utils";
/*
* Listings Locations — a centered heading above cards for each area: a photo, the name and how many listings it has.
* Source: marcframe/findit → components/sections/CitiesSection.tsx (+ design-system SectionTitle center, CityCard).
*
* Layout kept from Framer: one column below 768px, two from 768px; from 1024px a six-column grid where the cards run
* three to a row, then two to a row (spans 2, 2, 2, 3, 3, repeating). Rows 30px apart, columns 20px. Cards: a 300px
* photo, then the name and the count 5px apart. Each card is one link.
* Motion kept: the photo rests at 105% and settles to 100% while the card is hovered or focused — zoom-card, with the
* photo pre-scaled; Framer's 400ms tween where Findit uses a no-bounce spring. Dropped: Findit's dotted label.
*/
export type LocationCard = { name: string; href: string; count: string; image: { src: string; alt: string } };
export type ListingsLocationsProps = {
/** Small line above the heading. */
label?: string;
title?: string;
lead?: string;
locations?: LocationCard[];
className?: string;
};
const photo = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=1200&q=80`;
const spans = ["lg:col-span-2", "lg:col-span-2", "lg:col-span-2", "lg:col-span-3", "lg:col-span-3"];
const defaults = {
label: "Explore cities",
title: "Find a home where you want to live",
lead: "Every city has its own rhythm. Browse the places we know best and see what's available right now.",
locations: [
{ name: "New York", href: "/locations/new-york", count: "12 properties", image: { src: photo("photo-1480714378408-67cf0d13bc1b"), alt: "Manhattan skyline at sunset" } },
{ name: "Chicago", href: "/locations/chicago", count: "8 properties", image: { src: photo("photo-1477959858617-67f85cf4f1df"), alt: "Chicago skyline beside the lake" } },
{ name: "London", href: "/locations/london", count: "10 properties", image: { src: photo("photo-1513635269975-59663e0ac1ad"), alt: "The Thames winding through London, seen from above" } },
{ name: "Los Angeles", href: "/locations/los-angeles", count: "6 properties", image: { src: photo("photo-1444723121867-7a241cacace9"), alt: "City lights spreading to the horizon at night" } },
{ name: "Lower Manhattan", href: "/locations/lower-manhattan", count: "4 properties", image: { src: photo("photo-1514565131-fce0801e5785"), alt: "Lower Manhattan skyline across the water at dusk" } },
] as LocationCard[],
};
export function ListingsLocations({
label = defaults.label,
title = defaults.title,
lead = defaults.lead,
locations = defaults.locations,
className,
}: ListingsLocationsProps) {
return (
<section className={cn("py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter)">
<div className="mx-auto flex max-w-xl flex-col items-center gap-5 text-center">
<div className="flex flex-col items-center gap-2.5">
{label && <p className="text-sm font-medium text-muted-foreground">{label}</p>}
<h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2>
</div>
{lead && <p className="max-w-lg text-pretty text-muted-foreground">{lead}</p>}
</div>
<div className="grid gap-x-5 gap-y-7.5 md:grid-cols-2 lg:grid-cols-6">
{locations.map((location, i) => (
<Link
key={location.href}
href={location.href}
className={cn(
"group flex flex-col gap-2.5 rounded-xl outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
spans[i % spans.length],
)}
>
<ZoomCard zoom={0.952} className="h-75">
<img src={location.image.src} alt={location.image.alt} className="scale-105 object-cover" />
</ZoomCard>
<span className="flex flex-col gap-1">
<span className="font-heading text-lg font-semibold tracking-tight">{location.name}</span>
<span className="text-muted-foreground">{location.count}</span>
</span>
</Link>
))}
</div>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/listings-locations.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/listings-locations.tsx.import Link from "next/link"; import { ZoomCard } from "@/components/ui/zoom-card"; import { cn } from "@/lib/utils"; /* * Listings Locations — a centered heading above cards for each area: a photo, the name and how many listings it has. * Source: marcframe/findit → components/sections/CitiesSection.tsx (+ design-system SectionTitle center, CityCard). * * Layout kept from Framer: one column below 768px, two from 768px; from 1024px a six-column grid where the cards run * three to a row, then two to a row (spans 2, 2, 2, 3, 3, repeating). Rows 30px apart, columns 20px. Cards: a 300px * photo, then the name and the count 5px apart. Each card is one link. * Motion kept: the photo rests at 105% and settles to 100% while the card is hovered or focused — zoom-card, with the * photo pre-scaled; Framer's 400ms tween where Findit uses a no-bounce spring. Dropped: Findit's dotted label. */ export type LocationCard = { name: string; href: string; count: string; image: { src: string; alt: string } }; export type ListingsLocationsProps = { /** Small line above the heading. */ label?: string; title?: string; lead?: string; locations?: LocationCard[]; className?: string; }; const photo = (id: string) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=1200&q=80`; const spans = ["lg:col-span-2", "lg:col-span-2", "lg:col-span-2", "lg:col-span-3", "lg:col-span-3"]; const defaults = { label: "Explore cities", title: "Find a home where you want to live", lead: "Every city has its own rhythm. Browse the places we know best and see what's available right now.", locations: [ { name: "New York", href: "/locations/new-york", count: "12 properties", image: { src: photo("photo-1480714378408-67cf0d13bc1b"), alt: "Manhattan skyline at sunset" } }, { name: "Chicago", href: "/locations/chicago", count: "8 properties", image: { src: photo("photo-1477959858617-67f85cf4f1df"), alt: "Chicago skyline beside the lake" } }, { name: "London", href: "/locations/london", count: "10 properties", image: { src: photo("photo-1513635269975-59663e0ac1ad"), alt: "The Thames winding through London, seen from above" } }, { name: "Los Angeles", href: "/locations/los-angeles", count: "6 properties", image: { src: photo("photo-1444723121867-7a241cacace9"), alt: "City lights spreading to the horizon at night" } }, { name: "Lower Manhattan", href: "/locations/lower-manhattan", count: "4 properties", image: { src: photo("photo-1514565131-fce0801e5785"), alt: "Lower Manhattan skyline across the water at dusk" } }, ] as LocationCard[], }; export function ListingsLocations({ label = defaults.label, title = defaults.title, lead = defaults.lead, locations = defaults.locations, className, }: ListingsLocationsProps) { return ( <section className={cn("py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter)"> <div className="mx-auto flex max-w-xl flex-col items-center gap-5 text-center"> <div className="flex flex-col items-center gap-2.5"> {label && <p className="text-sm font-medium text-muted-foreground">{label}</p>} <h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2> </div> {lead && <p className="max-w-lg text-pretty text-muted-foreground">{lead}</p>} </div> <div className="grid gap-x-5 gap-y-7.5 md:grid-cols-2 lg:grid-cols-6"> {locations.map((location, i) => ( <Link key={location.href} href={location.href} className={cn( "group flex flex-col gap-2.5 rounded-xl outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", spans[i % spans.length], )} > <ZoomCard zoom={0.952} className="h-75"> <img src={location.image.src} alt={location.image.alt} className="scale-105 object-cover" /> </ZoomCard> <span className="flex flex-col gap-1"> <span className="font-heading text-lg font-semibold tracking-tight">{location.name}</span> <span className="text-muted-foreground">{location.count}</span> </span> </Link> ))} </div> </div> </section> ); }
Usage
import { ListingsLocations } from "@/components/listings-locations";
<ListingsLocations
locations={[
{ name: "Chicago", href: "/locations/chicago", count: "8 properties", image: { src: "/cities/chicago.jpg", alt: "Chicago skyline" } },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Explore cities | Small muted line above the heading. |
| title / lead | string | — | Centered heading (h2) and lead. |
| locations | { name, href, count, image: { src, alt } }[] | 5 cities | One link per card; five fill a 3 + 2 pattern from 1024px. |
| className | string | — | Classes for the <section>. |
- One column below 768px, two from 768px; from 1024px a six-column grid with cards spanning 2, 2, 2, 3, 3 (repeating).
- Photos rest at 105% and settle to 100% while the card is hovered or focused (zoom-card with the photo pre-scaled, 400ms).
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS block called ListingsLocations (Next.js). - <section className="py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter), column gap-12. - Centered title (max-w-xl): a small muted label, an h2 (font-heading text-h2 semibold), a muted lead up to max-w-lg. - Grid gap-x-5 gap-y-7.5: 1 column, 2 from 768px, 6 from 1024px where cards span 2, 2, 2, 3, 3 and repeat. - Card: one link (group, flex column gap-2.5): a 300px photo in a rounded-xl frame, the photo at scale 1.05 that eases to 1 while the card is hovered or focused (400ms, cubic-bezier(0.12,0.23,0.5,1)); then the name (font-heading text-lg semibold) and a muted count. - Only shadcn variables and block tokens; plain <img> with alt text.











