"use client";
import * as React from "react";
import { AirplaneWindowGallery } from "@/components/ui/airplane-window-gallery";
import type { WindowItem } from "@/components/ui/airplane-window-gallery";
const settings = {
windows: 2,
autoplay: true,
interval: 2100,
drift: 18,
shadeDuration: 400,
};
const VIEWS = [
{ id: "photo-1523632117739-12cc7fa95ad0", title: "South Malé Atoll", location: "Maldives", caption: "A sandbar curls through turquoise shallows." },
{ id: "photo-1636110330362-6f04cde3f1e4", title: "Mount Fuji", location: "Japan", caption: "The summit rises clean above the clouds." },
{ id: "photo-1615551043360-33de8b5f410c", title: "Grand Canyon", location: "United States", caption: "Layers of red rock and a winding river." },
{ id: "photo-1681407979901-566ba5099a0e", title: "Swiss Alps", location: "Switzerland", caption: "Snowy peaks seen from cruising altitude." },
{ id: "photo-1489516408517-0c0a15662682", title: "Dubai", location: "United Arab Emirates", caption: "The Palm and its crescent from above." },
{ id: "photo-1552561618-4fe029f7858a", title: "Glacial rivers", location: "Iceland", caption: "Meltwater braids across the valley floor." },
{ id: "photo-1789758612733-756075c4fb4c", title: "Great Barrier Reef", location: "Australia", caption: "Coral reefs scattered through clear blue water." },
{ id: "photo-1523374228107-6e44bd2b524e", title: "New York", location: "United States", caption: "The city lights up after dark." },
];
const ITEMS: WindowItem[] = VIEWS.map(({ id, title, location, caption }) => ({
src: `https://images.unsplash.com/${id}?w=900&h=920&fit=crop&q=80&auto=format`,
alt: `${title}, ${location}`,
title,
location,
caption,
}));
const CCGATHER_URL = "https://ccgather.com/?utm_source=21st&utm_medium=component&utm_campaign=airplane_window_gallery";
type DemoProps = {
windows?: number;
autoplay?: boolean;
interval?: number;
drift?: number;
shadeDuration?: number;
/** Set false to explore the gallery without the one-time guided zoom. */
tour?: boolean;
};
export default function Demo({ tour = true, ...props }: DemoProps = {}) {
const s = { ...settings, ...props };
const [activeWindow, setActiveWindow] = React.useState(null as number | null);
const [reduced, setReduced] = React.useState(true);
const [tourStopped, setTourStopped] = React.useState(false);
const [paused, setPaused] = React.useState(false);
const [replay, setReplay] = React.useState(0);
const [step, setStep] = React.useState("reveal");
const stopTour = () => setTourStopped(true);
React.useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReduced(media.matches);
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
React.useEffect(() => {
if (!tour || !s.autoplay || reduced || tourStopped || paused) return;
const change = window.setTimeout(() => setStep("change"), s.interval);
// Let the first replacement shade finish lifting before looking through that window.
const lookAt = s.interval + s.shadeDuration * 2 + 200;
const look = window.setTimeout(() => { setActiveWindow(0); setStep("lookout"); }, lookAt);
// 650ms to fly out, then 2.4 seconds outside before the 480ms return.
const back = window.setTimeout(() => { setActiveWindow(null); setStep("returned"); }, lookAt + 650 + 2000);
return () => { window.clearTimeout(change); window.clearTimeout(look); window.clearTimeout(back); };
}, [tour, s.autoplay, s.interval, s.shadeDuration, reduced, tourStopped, paused, replay]);
const restart = () => {
setActiveWindow(null);
setStep("reveal");
setTourStopped(false);
setPaused(false);
setReplay(replay + 1);
};
return (
<main
data-airplane-demo
data-tour={tour && s.autoplay && !reduced && !tourStopped && !paused}
onPointerDownCapture={stopTour}
onKeyDownCapture={stopTour}
onFocusCapture={(event) => { if ((event.target as HTMLElement).matches("button, a")) stopTour(); }}
className="flex min-h-screen w-full flex-col items-center justify-center gap-4 bg-background px-5 py-5 text-foreground"
>
<header className="text-center">
<p className="mb-1 text-[10px] font-medium uppercase tracking-[.22em] text-muted-foreground">The window seat</p>
<h1 className="text-2xl font-semibold tracking-tight">Airplane window gallery</h1>
<p className="mt-1.5 text-xs text-muted-foreground">Lift the shade. Fly through the window. Return to your seat.</p>
</header>
<AirplaneWindowGallery
key={replay}
items={ITEMS}
windows={s.windows}
autoplay={s.autoplay && !paused}
interval={s.interval}
drift={paused ? 0 : s.drift}
shadeDuration={s.shadeDuration}
activeWindow={activeWindow}
onActiveWindowChange={setActiveWindow}
className="max-w-[640px]"
/>
<div className="flex w-full max-w-[640px] flex-col items-center gap-2 text-center text-[11px] text-muted-foreground">
<p data-tour-caption>{activeWindow !== null ? "Click anywhere to sit back." : step === "change" && !tourStopped ? "A new destination, behind the same window." : step === "returned" ? "Back at your window seat. Fly out again." : "Click any window to look out."}</p>
<div className="flex items-center justify-center gap-3">
<button type="button" data-replay onClick={restart} className="rounded-sm text-foreground underline decoration-border underline-offset-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Replay preview</button>
{s.autoplay && !reduced && <button type="button" data-pause aria-pressed={paused} onClick={() => { setPaused(!paused); setTourStopped(true); }} className="rounded-sm underline decoration-border underline-offset-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">{paused ? "Resume motion" : "Pause motion"}</button>}
<span aria-hidden="true">·</span>
<a href={CCGATHER_URL} target="_blank" rel="noreferrer" className="rounded-sm underline decoration-border underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">By CCgather</a>
</div>
</div>
</main>
);
}