A design studio's contact card lit for the local time in each city. Switching from Lisbon to Tokyo carries the sun through the hours between.
registry/demos/golden-hour-studios-demo.tsx
"use client";
import * as React from "react";
import { GoldenHour } from "@/components/ui/golden-hour";
const STUDIOS = [
{ city: "Lisbon", zone: "Europe/Lisbon", address: "Rua das Flores 28", team: "6 in the studio", open: [9, 19] },
{ city: "New York", zone: "America/New_York", address: "55 Water St, Brooklyn", team: "11 in the studio", open: [9, 18] },
{ city: "Tokyo", zone: "Asia/Tokyo", address: "2-14 Kamimeguro, Meguro", team: "4 in the studio", open: [10, 19] },
] as const;
// The time now, read on the client only: the page is prerendered, so a server
// clock would be stale by the time anyone reads it.
const subscribe = (onChange: () => void) => {
const timer = window.setInterval(onChange, 30_000);
return () => window.clearInterval(timer);
};
const readMinute = () => Math.floor(Date.now() / 60_000);
const noMinute = () => null;
function hourIn(zone: string, minute: number) {
const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, hour: "numeric", minute: "numeric", hourCycle: "h23" }).formatToParts(
new Date(minute * 60_000),
);
const get = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? 0);
return get("hour") + get("minute") / 60;
}
function clock(hour: number) {
const minutes = Math.round(hour * 60) % (24 * 60);
const h = Math.floor(minutes / 60);
return `${((h + 11) % 12) + 1}:${String(minutes % 60).padStart(2, "0")} ${h < 12 ? "AM" : "PM"}`;
}
function hourLabel(hour: number) {
return `${((hour + 11) % 12) + 1} ${hour < 12 ? "AM" : "PM"}`;
}
export function GoldenHourStudiosDemo() {
const minute = React.useSyncExternalStore(subscribe, readMinute, noMinute);
const [picked, setPicked] = React.useState(0);
const tabs = React.useRef<(HTMLButtonElement | null)[]>([]);
const studio = STUDIOS[picked];
const hour = minute === null ? null : hourIn(studio.zone, minute);
const isOpen = hour !== null && hour >= studio.open[0] && hour < studio.open[1];
const pick = (index: number) => {
const next = (index + STUDIOS.length) % STUDIOS.length;
setPicked(next);
tabs.current[next]?.focus();
};
return (
<div className="w-full max-w-3xl">
<GoldenHour
id="golden-hour-studio"
role="tabpanel"
aria-labelledby={`golden-hour-studio-tab-${picked}`}
time={hour ?? "golden"}
className="flex aspect-[4/5] w-full items-end rounded-3xl p-4 sm:aspect-[16/10] sm:p-6"
>
<div className="w-full max-w-xs rounded-2xl bg-white/75 p-4 text-zinc-950 shadow-sm ring-1 ring-white/60 backdrop-blur-xl sm:p-5 dark:bg-zinc-950/55 dark:text-white dark:ring-white/10">
<p className="text-xs font-medium text-zinc-500 dark:text-zinc-400">Tinta · {studio.city} studio</p>
<p className="mt-1 text-4xl font-medium tracking-[-0.03em] tabular-nums">{hour === null ? "--:--" : clock(hour)}</p>
<p className="mt-1.5 flex items-center gap-1.5 text-sm text-zinc-600 dark:text-zinc-300">
<span
aria-hidden
className={`size-1.5 rounded-full ${isOpen ? "bg-emerald-500" : "bg-zinc-400 dark:bg-zinc-500"}`}
/>
{hour === null
? studio.team
: isOpen
? `Open until ${hourLabel(studio.open[1])} · ${studio.team}`
: `Closed · opens at ${hourLabel(studio.open[0])}`}
</p>
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">{studio.address}</p>
<a
href="#book"
className="mt-4 inline-flex h-9 items-center rounded-full bg-zinc-950 px-4 text-sm font-medium text-white transition-[background-color,scale] duration-150 ease-out hover:bg-zinc-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950 active:scale-[0.97] dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200 dark:focus-visible:outline-white"
>
{isOpen || hour === null ? "Book a call" : "Leave a note for the morning"}
</a>
</div>
</GoldenHour>
<div
role="tablist"
aria-label="Studios"
className="mt-4 grid grid-cols-3 gap-1 rounded-2xl bg-zinc-100 p-1 dark:bg-white/[0.06]"
onKeyDown={(event) => {
if (event.key === "ArrowRight") pick(picked + 1);
else if (event.key === "ArrowLeft") pick(picked - 1);
else return;
event.preventDefault();
}}
>
{STUDIOS.map((place, index) => {
const selected = index === picked;
return (
<button
key={place.city}
ref={(element) => {
tabs.current[index] = element;
}}
id={`golden-hour-studio-tab-${index}`}
type="button"
role="tab"
aria-selected={selected}
aria-controls="golden-hour-studio"
tabIndex={selected ? 0 : -1}
onClick={() => setPicked(index)}
className={`flex flex-col items-start rounded-xl px-3 py-2 text-left transition-[background-color,box-shadow] duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950 dark:focus-visible:outline-white ${
selected
? "bg-white shadow-sm ring-1 ring-zinc-950/5 dark:bg-white/10 dark:ring-white/10"
: "hover:bg-white/60 dark:hover:bg-white/[0.04]"
}`}
>
<span className="text-sm font-medium text-zinc-950 dark:text-white">{place.city}</span>
<span className="text-xs text-zinc-500 tabular-nums dark:text-zinc-400">
{minute === null ? "--:--" : clock(hourIn(place.zone, minute))}
</span>
</button>
);
})}
</div>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/golden-hour#golden-hour-studios-demo