/* global React, Tag, Icon, Eyebrow, Reveal */ const { useState, useMemo, useEffect, useRef } = React; // Distribute photos across N columns, deterministically. function distribute(items, cols) { const out = Array.from({ length: cols }, () => []); items.forEach((it, i) => out[i % cols].push(it)); return out; } function Gallery({ onOpenPhoto }) { const sectionRef = useRef(null); const [filter, setFilter] = useState('all'); const [inView, setInView] = useState(false); const all = window.TB_PHOTOS.gallery; const filtered = filter === 'all' ? all : all.filter((p) => p.genre === filter); // 4 vertical columns of marquee photos. We duplicate the list inside each // column so the loop is seamless. Columns alternate scroll direction. const columns = useMemo(() => distribute(filtered, 4), [filtered]); useEffect(() => { if (!sectionRef.current) return; const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), { rootMargin: '180px 0px' }); observer.observe(sectionRef.current); return () => observer.disconnect(); }, []); return (
selected New York photography ยท 2024 to 2026

twenty-four months,
mostly walking.

{[ { id: 'all', label: 'all' }, { id: 'portraits', label: 'portraits' }, { id: 'lifestyle', label: 'lifestyle' }, { id: 'behind-the-scenes', label: 'behind the scenes' } ].map((f) => setFilter(f.id)}>{f.label} )}
{/* 4-column vertical marquee grid */}
{columns.map((colItems, ci) => ( ))}
); } function MarqueeColumn({ items, direction, speed, onOpenPhoto, startOffset, active }) { // Duplicate the list once so the loop is seamless when translating by -50%. const doubled = [...items, ...items]; const animName = direction === 'up' ? 'tbMarqueeUp' : 'tbMarqueeDown'; return (
{doubled.map((p, i) => ( ))}
); } function GalleryTile({ photo, index, onOpenPhoto }) { // Varied aspect ratios so the column has a natural rhythm const ratios = ['4 / 5', '3 / 4', '1 / 1', '4 / 5', '3 / 4']; const ratio = photo.ratio || ratios[index % ratios.length]; return ( ); } window.Gallery = Gallery;