Round pins grow into photos. Swipe through the trip and the map pans from pin to pin behind the viewer.
import * as React from 'react'
import { landscapes } from '../../data/photos'
import { Arrows, Caption, Chrome, Lightbox, Slides, Toolbar } from '../lightbox'
// A road trip down an imaginary coast. x and y are pin positions on the 1200 × 900 map.
const stops = (
[
[0, 'Coast road', 560, 120],
[2, 'Rocky coast', 640, 260],
[5, 'Sea cliff', 520, 400],
[4, 'Shoreline', 600, 540],
[3, 'Road to the sea', 700, 660],
[6, 'Palm road', 790, 800],
] as const
).map(([i, caption, x, y]) => ({ ...landscapes[i]!, caption, x, y }))
const route = `M${stops.map((s) => `${s.x} ${s.y}`).join(' L')}`
export default function TravelMap() {
const map = React.useRef<HTMLDivElement>(null)
// Centre a stop in the map. The library's own page sync only scrolls a pin that is off screen; a map should follow.
const pan = (i: number, behavior: ScrollBehavior = 'smooth') => {
const el = map.current
const stop = stops[i]
if (el && stop) el.scrollTo({ left: stop.x - el.clientWidth / 2, top: stop.y - el.clientHeight / 2, behavior })
}
// biome-ignore lint/correctness/useExhaustiveDependencies: start on the first stop, once
React.useEffect(() => pan(0, 'instant'), [])
return (
<Lightbox.Root items={stops} onIndexChange={pan} scrollTriggerIntoView={false}>
<div ref={map} className="h-96 overflow-auto rounded-lg [scrollbar-width:none]">
<div className="relative h-[900px] w-[1200px]">
<svg viewBox="0 0 1200 900" className="absolute inset-0" aria-hidden>
<rect width="1200" height="900" className="fill-sky-200 dark:fill-sky-950" />
<path
d="M470 0C440 90 620 130 580 210S470 330 490 420 640 500 570 600 740 720 690 900H1200V0Z"
className="fill-amber-50 dark:fill-neutral-800"
/>
<ellipse cx="300" cy="320" rx="70" ry="40" className="fill-amber-50 dark:fill-neutral-800" />
<ellipse cx="220" cy="660" rx="40" ry="26" className="fill-amber-50 dark:fill-neutral-800" />
<ellipse cx="930" cy="300" rx="160" ry="110" className="fill-emerald-100 dark:fill-emerald-950" />
<ellipse cx="1000" cy="680" rx="130" ry="90" className="fill-emerald-100 dark:fill-emerald-950" />
<path
d={route}
fill="none"
strokeWidth="3"
strokeDasharray="2 10"
strokeLinecap="round"
className="stroke-neutral-500"
/>
</svg>
{stops.map((stop, i) => (
<div
key={stop.src}
className="absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-1"
style={{ left: stop.x, top: stop.y }}
>
<Lightbox.Trigger
index={i}
className="size-14 overflow-hidden rounded-full border-2 border-white bg-white shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-sky-500"
>
<img src={stop.thumb} alt={stop.caption} className="size-full rounded-full object-cover" />
</Lightbox.Trigger>
<span className="rounded-full bg-white/90 px-2 py-0.5 text-xs font-medium whitespace-nowrap text-neutral-900 shadow-sm">
{stop.caption}
</span>
</div>
))}
</div>
</div>
<Lightbox.Portal>
{/* See-through, so the map panning from pin to pin stays visible around the photo. */}
<Lightbox.Backdrop className="z-50 bg-neutral-950/50" />
<Lightbox.Popup className="group z-50 text-white outline-none">
<Slides className="px-4 pt-16 pb-28 sm:px-24 sm:pt-20 sm:pb-32" imageClassName="rounded-2xl shadow-2xl" />
<Chrome>
<Toolbar />
<Arrows />
<Caption />
</Chrome>
</Lightbox.Popup>
</Lightbox.Portal>
</Lightbox.Root>
)
}