World Clock Bar

Multi-timezone display for follow-the-sun teams and operational handoffs.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/world-clock-bar.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

"use client"; import * as React from "react"; import type { HeadingTag } from "../../lib/types"; import { cn } from "../../lib/utils"; import { Badge } from "../badge/badge"; export type WorldClockBarZone = { city: string; locale?: string; timeZone: string; }; export type WorldClockBarProps = React.ComponentPropsWithoutRef<"div"> & { /** Heading tag for the title. Defaults to `h2`. */ as?: HeadingTag; now?: Date | number | string; showDate?: boolean; title?: string; updateIntervalMs?: number; zones: WorldClockBarZone[]; }; function normalizeDate(input: Date | number | string): Date { if (input instanceof Date) { return new Date(input.getTime()); } return new Date(input); } function useLiveDate(now: WorldClockBarProps["now"], updateIntervalMs: number) { const fixedNow = React.useMemo( () => (now ? normalizeDate(now) : undefined), [now], ); const [liveNow, setLiveNow] = React.useState<Date>(fixedNow ?? new Date()); React.useEffect(() => { if (fixedNow) { setLiveNow(fixedNow); return; } const interval = window.setInterval(() => { setLiveNow(new Date()); }, updateIntervalMs); return () => { window.clearInterval(interval); }; }, [fixedNow, updateIntervalMs]); return liveNow; } const TIME_FORMATTER_CACHE = new Map<string, Intl.DateTimeFormat>(); function getTimeFormatter( locale: string, timeZone: string, ): Intl.DateTimeFormat { const key = `${locale}|${timeZone}`; let formatter = TIME_FORMATTER_CACHE.get(key); if (!formatter) { formatter = Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone, timeZoneName: "short", }); TIME_FORMATTER_CACHE.set(key, formatter); } return formatter; } const DATE_FORMATTER_CACHE = new Map<string, Intl.DateTimeFormat>(); function getDateFormatter( locale: string, timeZone: string, ): Intl.DateTimeFormat { const key = `${locale}|${timeZone}`; let formatter = DATE_FORMATTER_CACHE.get(key); if (!formatter) { formatter = Intl.DateTimeFormat(locale, { day: "numeric", month: "short", timeZone, weekday: "short", }); DATE_FORMATTER_CACHE.set(key, formatter); } return formatter; } function formatZoneDateTime( zone: WorldClockBarZone, date: Date, showDate: boolean, ) { const locale = zone.locale ?? "en-US"; const timeFormatter = getTimeFormatter(locale, zone.timeZone); const dateFormatter = getDateFormatter(locale, zone.timeZone); return { date: showDate ? dateFormatter.format(date) : "", time: timeFormatter.format(date), }; } function WorldClockCard({ date, showDate, zone, }: { date: Date; showDate: boolean; zone: WorldClockBarZone; }) { const formatted = formatZoneDateTime(zone, date, showDate); return ( <div className="min-w-[190px] rounded-lg border bg-card px-4 py-3 shadow-sm"> <div className="text-sm font-medium">{zone.city}</div> <div className="mt-1 text-2xl font-semibold tracking-tight"> {formatted.time} </div> {showDate ? ( <div className="mt-1 text-xs text-muted-foreground"> {formatted.date} </div> ) : null} <div className="mt-3 text-[11px] uppercase tracking-[0.16em] text-muted-foreground"> {zone.timeZone} </div> </div> ); } export const WorldClockBar = React.forwardRef< HTMLDivElement, WorldClockBarProps >( ( { as: Heading = "h2", className, now, showDate = true, title = "World clock", updateIntervalMs = 60_000, zones, ...props }, ref, ) => { const liveNow = useLiveDate(now, updateIntervalMs); return ( <div className={cn("space-y-3", className)} ref={ref} {...props}> <div className="flex items-center justify-between gap-3"> <div> <Heading className="text-lg font-semibold tracking-tight"> {title} </Heading> <p className="text-sm text-muted-foreground"> Synchronized time across distributed teams and regions. </p> </div> <Badge variant="outline">{zones.length} zones</Badge> </div> <div className="flex gap-3 overflow-x-auto pb-1"> {zones.map((zone) => ( <WorldClockCard date={liveNow} key={`${zone.city}-${zone.timeZone}`} showDate={showDate} zone={zone} /> ))} </div> </div> ); }, ); WorldClockBar.displayName = "WorldClockBar";

Dependencies

  • @vllnt/ui@^0.2.1