{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "contact-offices",
  "title": "Contact Offices",
  "description": "One tile per office with address, phone, weekly hours, local time and an open or closed signal in words.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/contact-offices/contact-offices.tsx",
      "content": "// Contact offices: one tile per office with address, phone, weekly hours, local time and an open/closed signal.\n// Signature: the open office carries the one --primary dot; the open office closest to closing says \"Closes in\n// 40 min\" in mono; closed offices say when they open next (\"Opens Mon 09:00\").\n// Screen readers: a list of offices with an h3 each; the status is text, never colour alone; hours are a table.\n// Time: pass `now` for a fixed time; otherwise the section renders a neutral placeholder on the server and reads\n// the clock in an effect after mount, ticking once a minute. Reduced motion: nothing animates.\nimport { useEffect, useId, useRef, useState, type HTMLAttributes, type ReactNode } from 'react'\nimport { Check, Copy, MapPin } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport type OfficeHours = {\n  /** Day of week, 0 = Sunday ... 6 = Saturday. */\n  day: number\n  /** \"09:00\" in the office's local time. */\n  open: string\n  /** \"17:00\" in the office's local time, later than `open` on the same day (\"24:00\" for midnight). For hours that run\n   * past midnight, give two rows: one to \"24:00\" and one from \"00:00\" on the next day. */\n  close: string\n}\n\nexport type ContactOffice = {\n  id: string\n  city: string\n  address: string\n  phone?: string\n  /** IANA zone, e.g. \"Europe/Berlin\". */\n  timeZone: string\n  hours: OfficeHours[]\n  /** Link to a map or directions. */\n  directionsHref?: string\n}\n\nexport type ContactOfficesProps = Omit<HTMLAttributes<HTMLElement>, 'title'> & {\n  heading: ReactNode\n  intro?: ReactNode\n  offices: ContactOffice[]\n  /** A fixed time; when absent the clock runs after mount. */\n  now?: Date\n  labels?: {\n    open?: string; closed?: string; localTime?: string; copyAddress?: string; copied?: string; failed?: string\n    directions?: string; newTab?: string; hours?: string; day?: string; closesIn?: (min: number) => string; opensAt?: (day: string, time: string) => string\n    days?: string[]\n  }\n}\n\nconst DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\nconst toMin = (t: string) => {\n  const [h, m] = t.split(':').map(Number)\n  return h * 60 + (m || 0)\n}\n\nfunction localParts(now: Date, timeZone: string) {\n  const f = new Intl.DateTimeFormat('en-GB', { timeZone, weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })\n  const p = Object.fromEntries(f.formatToParts(now).map((x) => [x.type, x.value]))\n  return { day: DAYS.indexOf(p.weekday), minutes: Number(p.hour) * 60 + Number(p.minute), clock: `${p.hour}:${p.minute}` }\n}\n\ntype OfficeStatus = { open: boolean; clock: string; closesIn?: number; next?: { day: number; time: string } }\n\n/** Whether an office is open at `now`, how long until it closes, or when it opens next. */\nfunction officeStatus(o: ContactOffice, now: Date): OfficeStatus {\n  const { day, minutes, clock } = localParts(now, o.timeZone)\n  const today = o.hours.find((h) => h.day === day && minutes >= toMin(h.open) && minutes < toMin(h.close))\n  if (today) return { open: true, clock, closesIn: toMin(today.close) - minutes }\n  for (let i = 0; i < 8; i++) {\n    const d = (day + i) % 7\n    const h = o.hours.filter((x) => x.day === d).sort((a, b) => toMin(a.open) - toMin(b.open)).find((x) => i > 0 || toMin(x.open) > minutes)\n    if (h) return { open: false, clock, next: { day: d, time: h.open } }\n  }\n  return { open: false, clock }\n}\n\nfunction CopyAddress({ text, L }: { text: string; L: { copyAddress: string; copied: string; failed: string } }) {\n  const [s, setS] = useState<'idle' | 'copied' | 'failed'>('idle')\n  const t = useRef<ReturnType<typeof setTimeout>>(undefined)\n  useEffect(() => () => clearTimeout(t.current), [])\n  const run = async () => {\n    try {\n      await navigator.clipboard.writeText(text)\n      setS('copied')\n    } catch {\n      setS('failed')\n    }\n    clearTimeout(t.current)\n    t.current = setTimeout(() => setS('idle'), 1800)\n  }\n  return (\n    <>\n      <button type=\"button\" onClick={run} className=\"inline-flex min-h-11 items-center gap-1.5 rounded-full bg-muted px-3.5 text-sm font-medium outline-offset-2 focus-visible:outline-2 focus-visible:outline-ring\">\n        {s === 'copied' ? <Check className=\"size-4\" aria-hidden /> : <Copy className=\"size-4\" aria-hidden />}\n        {s === 'copied' ? L.copied : s === 'failed' ? L.failed : L.copyAddress}\n      </button>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">{s === 'copied' ? L.copied : s === 'failed' ? L.failed : ''}</span>\n    </>\n  )\n}\n\nexport function ContactOffices({ heading, intro, offices, now, labels = {}, className, ...rest }: ContactOfficesProps) {\n  const L = {\n    open: 'Open now', closed: 'Closed', localTime: 'Local time', copyAddress: 'Copy address', copied: 'Copied', failed: 'Not copied',\n    directions: 'Directions', newTab: '(opens in a new tab)', hours: 'Opening hours',\n    closesIn: (m: number) => (m >= 60 ? `Closes in ${Math.floor(m / 60)} h ${m % 60 ? `${m % 60} min` : ''}`.trim() : `Closes in ${m} min`),\n    opensAt: (d: string, t: string) => `Opens ${d} ${t}`,\n    days: DAYS,\n    ...labels,\n  }\n  const uid = useId()\n  const [clock, setClock] = useState<Date | null>(null)\n  useEffect(() => {\n    if (now) return\n    // Read after mount, then again on each new minute (not on a timer from mount, which would lag up to 59 s).\n    let timer = setTimeout(function tick() {\n      setClock(new Date())\n      timer = setTimeout(tick, 60_000 - (Date.now() % 60_000) + 20)\n    }, 0)\n    return () => clearTimeout(timer)\n  }, [now])\n  const at = now ?? clock\n  const statuses = at ? offices.map((o) => officeStatus(o, at)) : null\n  const soonest = statuses\n    ? statuses.reduce<number>((best, s, i) => (s.open && (best < 0 || (s.closesIn ?? 1e9) < (statuses[best].closesIn ?? 1e9)) ? i : best), -1)\n    : -1\n\n  return (\n    <section aria-labelledby={`${uid}-h`} className={cn('@container mx-auto w-full max-w-6xl px-4 py-12 sm:px-6', className)} {...rest}>\n      <header className=\"max-w-2xl\">\n        <h2 id={`${uid}-h`} className=\"font-[family-name:var(--font-display,inherit)] text-3xl font-semibold tracking-tight text-balance @min-[40rem]:text-4xl\">{heading}</h2>\n        {intro && <p className=\"mt-3 text-muted-foreground\">{intro}</p>}\n      </header>\n      <ul className=\"mt-8 grid list-none gap-3 p-0 @min-[44rem]:grid-cols-2\">\n        {offices.map((o, i) => {\n          const st = statuses?.[i]\n          return (\n            <li key={o.id} className=\"min-w-0\">\n              <article className=\"flex h-full flex-col rounded-[26px] bg-card p-6 text-card-foreground shadow-[0_0_0_1px_color-mix(in_oklab,var(--foreground)_9%,transparent),0_1px_2px_rgba(0,0,0,0.03)]\">\n                <div className=\"flex items-start justify-between gap-3\">\n                  <h3 className=\"font-[family-name:var(--font-display,inherit)] text-2xl font-semibold\">{o.city}</h3>\n                  <span className=\"flex items-center gap-2 rounded-full bg-muted px-3 py-1 text-xs font-medium\" aria-live=\"polite\">\n                    <span aria-hidden className={cn('size-2 rounded-full', st?.open ? 'bg-primary' : 'bg-muted-foreground/50')} />\n                    {st ? (st.open ? L.open : L.closed) : <span className=\"inline-block h-3 w-14\" aria-hidden />}\n                  </span>\n                </div>\n                <p className=\"mt-1 font-mono text-xs tabular-nums text-muted-foreground\">\n                  {L.localTime} {st ? st.clock : '--:--'} · {o.timeZone}\n                </p>\n                {st && (\n                  <p className=\"mt-3 min-h-5 font-mono text-xs tabular-nums\">\n                    {st.open && i === soonest && st.closesIn !== undefined && st.closesIn <= 120 && L.closesIn(st.closesIn)}\n                    {!st.open && st.next && L.opensAt(L.days[st.next.day], st.next.time)}\n                  </p>\n                )}\n                <address className=\"mt-3 text-sm not-italic\">\n                  {o.address}\n                  {o.phone && (<><br /><a className=\"inline-flex min-h-11 items-center underline underline-offset-4 outline-offset-2 focus-visible:outline-2 focus-visible:outline-ring tabular-nums\" href={`tel:${o.phone.replace(/[^\\d+]/g, '')}`}>{o.phone}</a></>)}\n                </address>\n                <table className=\"mt-4 w-full text-left font-mono text-xs tabular-nums text-muted-foreground\">\n                  <caption className=\"sr-only\">{L.hours}</caption>\n                  <tbody>\n                    {[...o.hours].sort((a, b) => ((a.day + 6) % 7) - ((b.day + 6) % 7)).map((h) => (\n                      <tr key={h.day + h.open}>\n                        <th scope=\"row\" className=\"py-0.5 pr-3 font-normal\">{L.days[h.day]}</th>\n                        <td className=\"py-0.5\">{h.open} to {h.close}</td>\n                      </tr>\n                    ))}\n                  </tbody>\n                </table>\n                <div className=\"mt-auto flex flex-wrap gap-2 pt-5\">\n                  <CopyAddress text={`${o.city}, ${o.address}`} L={L} />\n                  {o.directionsHref && (\n                    <a href={o.directionsHref} target=\"_blank\" rel=\"noopener noreferrer\" className=\"inline-flex min-h-11 items-center gap-1.5 rounded-full bg-primary px-4 text-sm font-medium text-primary-foreground outline-offset-2 focus-visible:outline-2 focus-visible:outline-ring\">\n                      <MapPin className=\"size-4\" aria-hidden />{L.directions}<span className=\"sr-only\"> {o.city} {L.newTab}</span>\n                    </a>\n                  )}\n                </div>\n              </article>\n            </li>\n          )\n        })}\n      </ul>\n    </section>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "templates"
  ],
  "type": "registry:block"
}