{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "footer-desk",
  "title": "Footer Desk",
  "description": "A front-desk footer: one inverted tile with a contact person, a way to reach them and office clocks that tick on the minute.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "@manniche/data-tile"
  ],
  "files": [
    {
      "path": "registry/manniche/footer-desk/footer-desk.tsx",
      "content": "// A \"front desk\" footer: one inverted tile that puts a person, a way to reach them and the time where they are\n// in front of the visitor. Signature: the office clocks, set in mono, which tick on the minute (not on a timer\n// that drifts) and say \"Same time\" when every office is in the same offset.\n// Screen readers: a <footer> with one h2, the contact person as plain text, the offices as a list where each time\n// is a <time> element. The clocks never announce themselves. Copy results go through a polite live region.\n// Reduced motion: the copy icon swaps with opacity only, nothing else moves.\nimport { ArrowUpRight, Check, Copy } from 'lucide-react'\nimport { useEffect, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\nimport { DataTile } from '@/registry/manniche/data-tile/data-tile'\n\nexport type DeskOffice = {\n  /** The place, as shown: \"Munich\". */\n  city: string\n  /** An IANA time zone: \"Europe/Berlin\". */\n  timeZone: string\n}\n\nexport type FooterDeskProps = Omit<ComponentPropsWithoutRef<'footer'>, 'children'> & {\n  /** The display line of the section (the h2): \"Talk to a person.\" */\n  heading: ReactNode\n  /** One quiet sentence under the heading. */\n  description?: ReactNode\n  /** The person at the desk. */\n  contact: {\n    name: string\n    role: string\n    /** One or two letters for the avatar. Defaults to the initials of the name. */\n    initials?: string\n    /** Replaces the initials, for a photo. Give it an empty alt: the name is written out next to it. */\n    avatar?: ReactNode\n  }\n  /** The address that gets the copy button. */\n  email: string\n  /** \"Book a call\". Left out, the link is left out. */\n  bookCall?: { label: string; href: string; external?: boolean }\n  /** The offices whose local time is shown, in the order given. */\n  offices: DeskOffice[]\n  /** The moment to show. Left out, the clocks read the real time after mount and tick once a minute. */\n  now?: Date\n  /** BCP 47 locale for the times. Default: the visitor's. */\n  locale?: string\n  /** 12-hour clocks. Default false. */\n  hour12?: boolean\n  /** A quiet line under the tile: ©, legal links. */\n  legal?: ReactNode\n  /** Interface strings, for translation. */\n  labels?: {\n    /** Name of the group of clocks. */\n    offices?: string\n    /** Shown when all offices share one offset. */\n    sameTime?: string\n    /** Shown instead of a time before the clock has been read. */\n    pending?: string\n    copy?: string\n    copied?: string\n    copyFailed?: string\n    /** Hidden text after an external link. */\n    newTab?: string\n  }\n}\n\nconst L = {\n  offices: 'Local time',\n  sameTime: 'Same time',\n  pending: '--:--',\n  copy: 'Copy email',\n  copied: 'Copied',\n  copyFailed: 'Not copied. Select the address instead.',\n  newTab: '(opens in a new tab)',\n}\n\n/** Minutes the zone is ahead of UTC at this moment, or null for a zone the browser does not know. */\nfunction offsetMinutes(at: Date, timeZone: string): number | null {\n  try {\n    const parts = new Intl.DateTimeFormat('en-US', {\n      timeZone,\n      hourCycle: 'h23',\n      year: 'numeric',\n      month: 'numeric',\n      day: 'numeric',\n      hour: 'numeric',\n      minute: 'numeric',\n      second: 'numeric',\n    }).formatToParts(at)\n    const n = (t: string) => Number(parts.find((p) => p.type === t)?.value)\n    const wall = Date.UTC(n('year'), n('month') - 1, n('day'), n('hour'), n('minute'), n('second'))\n    return Math.round((wall - Math.floor(at.getTime() / 1000) * 1000) / 60000)\n  } catch {\n    return null\n  }\n}\n\nfunction clock(at: Date, timeZone: string, locale: string | undefined, hour12: boolean) {\n  try {\n    return new Intl.DateTimeFormat(locale, { timeZone, hour: '2-digit', minute: '2-digit', hour12 }).format(at)\n  } catch {\n    return null\n  }\n}\n\n/** The current time, read after mount and refreshed exactly on each minute. A fixed `now` is used as given. */\nfunction useMinuteClock(fixed?: Date) {\n  const [at, setAt] = useState<Date | null>(null)\n  useEffect(() => {\n    if (fixed) return\n    let timer: ReturnType<typeof setTimeout>\n    const tick = () => {\n      setAt(new Date())\n      timer = setTimeout(tick, 60000 - (Date.now() % 60000) + 20)\n    }\n    tick()\n    return () => clearTimeout(timer)\n  }, [fixed])\n  return fixed ?? at\n}\n\nfunction initialsOf(name: string) {\n  const words = name.trim().split(/\\s+/)\n  return ((words[0]?.[0] ?? '') + (words.length > 1 ? (words.at(-1)?.[0] ?? '') : '')).toUpperCase()\n}\n\nconst pill =\n  'inline-flex min-h-11 items-center gap-2 rounded-full px-5 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'\n\nexport function FooterDesk({\n  heading,\n  description,\n  contact,\n  email,\n  bookCall,\n  offices,\n  now,\n  locale,\n  hour12 = false,\n  legal,\n  labels,\n  className,\n  ...rest\n}: FooterDeskProps) {\n  const t = { ...L, ...labels }\n  const at = useMinuteClock(now)\n  const [copy, setCopy] = useState<'idle' | 'copied' | 'failed'>('idle')\n  const reset = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n  useEffect(() => () => clearTimeout(reset.current), [])\n\n  const copyEmail = async () => {\n    try {\n      await navigator.clipboard.writeText(email)\n      setCopy('copied')\n    } catch {\n      setCopy('failed')\n    }\n    clearTimeout(reset.current)\n    reset.current = setTimeout(() => setCopy('idle'), 2400)\n  }\n\n  const offsets = at ? offices.map((o) => offsetMinutes(at, o.timeZone)) : []\n  const same = offices.length > 1 && offsets.length > 0 && offsets.every((o) => o !== null && o === offsets[0])\n\n  return (\n    <footer className={cn('mx-auto w-full max-w-6xl px-4 sm:px-6', className)} {...rest}>\n      <div className=\"@container\">\n        <DataTile inverted className=\"overflow-hidden\">\n          <div className=\"grid gap-10 py-2 @min-[56rem]:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)] @min-[56rem]:gap-14 @min-[56rem]:py-4\">\n            <div className=\"flex min-w-0 flex-col gap-8\">\n              <div className=\"grid gap-4\">\n                <h2\n                  className=\"text-[clamp(32px,6.4cqi,64px)] leading-[0.98] font-extrabold tracking-[-0.04em] text-balance\"\n                  style={{ fontFamily: 'var(--font-display, inherit)', fontStretch: '86%' }}\n                >\n                  {heading}\n                </h2>\n                {description && <p className=\"max-w-[44ch] text-[15px] leading-relaxed text-pretty text-muted-foreground\">{description}</p>}\n              </div>\n\n              <div className=\"flex items-center gap-3.5\">\n                <span\n                  aria-hidden={contact.avatar ? undefined : true}\n                  className=\"grid size-12 flex-none place-items-center overflow-hidden rounded-full bg-muted font-mono text-[13px] font-medium tracking-wide\"\n                >\n                  {contact.avatar ?? (contact.initials ?? initialsOf(contact.name))}\n                </span>\n                <p className=\"grid min-w-0 gap-0.5\">\n                  <span className=\"text-[15px] leading-tight font-medium\">{contact.name}</span>\n                  <span className=\"text-sm leading-tight text-muted-foreground\">{contact.role}</span>\n                </p>\n              </div>\n\n              <div className=\"flex flex-wrap items-center gap-x-3 gap-y-3\">\n                <span className=\"inline-flex min-h-11 max-w-full items-center rounded-full bg-muted pr-1.5 pl-4\">\n                  <a\n                    href={`mailto:${email}`}\n                    className=\"flex min-h-11 min-w-0 items-center rounded-sm font-mono text-[13px] underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-ring\"\n                  >\n                    <span className=\"truncate\">{email}</span>\n                  </a>\n                  <button\n                    type=\"button\"\n                    onClick={copyEmail}\n                    className=\"relative ml-2 grid size-9 flex-none cursor-pointer place-items-center rounded-full text-muted-foreground transition-[opacity] duration-200 ease-out-quint hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-ring before:absolute before:-inset-1 before:content-['']\"\n                  >\n                    <span className=\"sr-only\">{t.copy}</span>\n                    <Copy aria-hidden className={cn('absolute size-4 transition-[opacity] duration-200 ease-out-quint', copy === 'copied' ? 'opacity-0' : 'opacity-100')} />\n                    <Check aria-hidden className={cn('absolute size-4 transition-[opacity] duration-200 ease-out-quint', copy === 'copied' ? 'opacity-100' : 'opacity-0')} />\n                  </button>\n                </span>\n                {bookCall && (\n                  <a\n                    href={bookCall.href}\n                    {...(bookCall.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n                    className={cn(pill, 'bg-foreground text-background transition-[transform] duration-200 ease-out-quint active:scale-[0.98]')}\n                  >\n                    {bookCall.label}\n                    <ArrowUpRight aria-hidden className=\"size-4\" />\n                    {bookCall.external && <span className=\"sr-only\"> {t.newTab}</span>}\n                  </a>\n                )}\n                <span role=\"status\" className={cn('text-[13px] text-muted-foreground', copy === 'idle' && 'sr-only')}>\n                  {copy === 'copied' ? t.copied : copy === 'failed' ? t.copyFailed : ''}\n                </span>\n              </div>\n            </div>\n\n            <div className=\"grid min-w-0 content-start gap-3\">\n              <div className=\"flex min-h-6 flex-wrap items-center justify-between gap-x-3 gap-y-1\">\n                <h3 className=\"font-mono text-[11px] tracking-[0.08em] text-muted-foreground uppercase\">{t.offices}</h3>\n                {same && (\n                  <span className=\"inline-flex items-center gap-1.5 rounded-full bg-muted px-2.5 py-1 font-mono text-[11px] leading-none\">\n                    <Check aria-hidden className=\"size-3\" />\n                    {t.sameTime}\n                  </span>\n                )}\n              </div>\n              <ul className=\"grid gap-3 @min-[26rem]:grid-cols-2 @min-[56rem]:grid-cols-1\">\n                {offices.map((o, i) => {\n                  const text = at ? clock(at, o.timeZone, locale, hour12) : null\n                  const off = offsets[i]\n                  return (\n                    <li key={`${o.city}-${o.timeZone}`} className=\"flex items-baseline justify-between gap-4 rounded-[14px] bg-muted px-4 py-3.5\">\n                      <span className=\"grid min-w-0 gap-0.5\">\n                        <span className=\"text-[15px] leading-tight font-medium\">{o.city}</span>\n                        <span className=\"font-mono text-[11px] leading-tight text-muted-foreground tabular-nums\">\n                          {off === null || off === undefined ? o.timeZone : `UTC${off >= 0 ? '+' : '−'}${Math.floor(Math.abs(off) / 60)}${Math.abs(off) % 60 ? `:${String(Math.abs(off) % 60).padStart(2, '0')}` : ''}`}\n                        </span>\n                      </span>\n                      <time\n                        dateTime={at ? at.toISOString() : undefined}\n                        className=\"font-mono text-[clamp(24px,4cqi,32px)] leading-none font-medium tracking-tight tabular-nums\"\n                      >\n                        {text ?? t.pending}\n                      </time>\n                    </li>\n                  )\n                })}\n              </ul>\n            </div>\n          </div>\n        </DataTile>\n        {legal && <div className=\"flex flex-wrap items-center gap-x-5 gap-y-2 px-2 pt-5 pb-8 text-[13px] text-muted-foreground\">{legal}</div>}\n      </div>\n    </footer>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "templates"
  ],
  "type": "registry:block"
}