Copy for LLM
Masonry
A staggered, vertically-scrolling multi-column layout (the Pinterest / Photos look, where columns do not line up). Drop any children in; Masonry measures each and fills the shortest column, then keeps a D-pad-focused child in view.
Installation
npx @glasskit-ui/cli add masonryInstall the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:
npm install @glasskit-ui/react// components/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";export type { ClassValue };/** * Merge class names the shadcn way: clsx joins conditionals, tailwind-merge * de-dupes conflicting Tailwind utilities so a consumer's `className` override * wins (e.g. passing `px-2` beats the component's `px-6`). Lens components are * Tailwind utilities + `--gk-*` tokens, so this de-dupe matters. */export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs));}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined { return typeof label === "string" ? label : undefined;}// components/glasskit/masonry.tsx"use client";import { Children, useLayoutEffect, useMemo, useRef, useState, type ReactNode,} from "react";import { cn } from "../lib/utils";/** * <Masonry> — a staggered, vertically-scrolling multi-column layout (the * Pinterest / Photos gallery look, where columns do not line up). Drop any * children in (MediaThumb tiles, Pressable cards, anything); Masonry measures * each one and greedily fills the shortest column so the sides stay balanced * whatever the order. It scrolls vertically and keeps a D-pad-focused child in * view, so a gallery is one line. * * This is layout only: the children own their own interactivity (e.g. a * MediaThumb with `onSelect`, or a `<Pressable>`). */export function Masonry({ columns = 2, children, className,}: { /** Number of columns. */ columns?: number; children: ReactNode; className?: string;}) { const items = useMemo(() => Children.toArray(children), [children]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const scrollRef = useRef<HTMLDivElement>(null); // Start round-robin (a sane SSR/first-paint split), then rebalance by height. const [assign, setAssign] = useState<number[]>(() => items.map((_, i) => i % columns), ); // Measure each item and deal it to the currently shortest column. useLayoutEffect(() => { const colH = new Array(columns).fill(0); const next = itemRefs.current.slice(0, items.length).map((el) => { let c = 0; for (let j = 1; j < columns; j++) if (colH[j]! < colH[c]!) c = j; colH[c]! += el?.offsetHeight ?? 0; return c; }); setAssign((prev) => prev.length === next.length && prev.every((v, i) => v === next[i]) ? prev : next, ); }, [columns, items.length]); // Keep a D-pad-focused child scrolled into view (focusin bubbles up). useLayoutEffect(() => { const el = scrollRef.current; if (!el) return; const onFocusIn = (e: FocusEvent) => { const t = e.target as HTMLElement | null; if (t && el.contains(t) && typeof t.scrollIntoView === "function") { t.scrollIntoView({ block: "nearest", behavior: "smooth" }); } }; el.addEventListener("focusin", onFocusIn); return () => el.removeEventListener("focusin", onFocusIn); }, []); const cols: ReactNode[][] = Array.from({ length: columns }, () => []); items.forEach((child, i) => { cols[assign[i] ?? i % columns]!.push( <div key={i} className="gk-masonry__item" ref={(el) => { itemRefs.current[i] = el; }} > {child} </div>, ); }); return ( <div ref={scrollRef} className={cn("gk-masonry", className)}> {cols.map((col, i) => ( <div className="gk-masonry__col" key={i}> {col} </div> ))} </div> );}Usage
<Masonry columns={2}> {photos.map((p) => ( <MediaThumb key={p.id} src={p.src} aspect={p.aspect} onSelect={() => open(p)} /> ))}</Masonry>Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | number | 2 | Number of columns. |
children | ReactNode | — | The items (e.g. MediaThumb tiles or Pressable cards). |
When to use
Reach for Masonry when your tiles vary in height and you want them packed tight without the rows lining up: a photo gallery, a feed of mixed cards. It measures each child and deals it to the currently shortest column, so the two sides stay balanced whatever order the items come in, and it scrolls vertically.
For cells that should line up in a uniform grid, use Grid
instead. Masonry is layout only: the children own their interactivity (a
MediaThumb with onSelect, or a
Pressable).
Grid
An aligned, vertically-scrolling multi-column layout: every cell shares the same track, so rows and columns line up. Drop any children in; it scrolls vertically and keeps a D-pad-focused child in view.
Heading
A screen/section title with an optional accent eyebrow above it. Pure display: one heading per view keeps the glance cheap.