World Clock Bar
Multi-timezone display for follow-the-sun teams and operational handoffs. Install World Clock Bar from the VLLNT UI registry with the shadcn CLI.
Multi-timezone display for follow-the-sun teams and operational handoffs. Part of the data family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add World Clock Bar to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/world-clock-bar.jsonSource
"use client";
import * as React from "react";
import type { HeadingTag } from "../../lib/types";
import { useLiveDate } from "../../lib/use-live-date";
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[];
};
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 = ({
as: Heading = "h2",
className,
now,
ref,
showDate = true,
title = "World clock",
updateIntervalMs = 60_000,
zones,
...props
}: WorldClockBarProps & { ref?: React.Ref<HTMLDivElement> }) => {
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";
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.