{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agenda",
  "type": "registry:ui",
  "title": "Agenda",
  "description": "Upcoming events by day with a Now line and a Join that counts down.",
  "categories": [
    "scheduling"
  ],
  "dependencies": [
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/agenda.tsx",
      "content": "\"use client\";\nimport NumberFlow, { NumberFlowGroup } from \"@number-flow/react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ArrowDown, ArrowUp, ChevronDown, Plus } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type EventTone = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\n\nexport type AgendaEvent = {\n  id: string;\n  title: string;\n  start: Date;\n  /** Exclusive. */\n  end: Date;\n  allDay?: boolean;\n  location?: string;\n  /** A video call link. Shows a Join button until the event ends. */\n  meetingUrl?: string;\n  /** Short line under the title: attendees, a project, a room. */\n  detail?: string;\n  /** Meaning only: a deadline, an outage, a launch. */\n  tone?: EventTone;\n  status?: \"confirmed\" | \"tentative\" | \"canceled\";\n};\n\n/* ------------------------------------------------------------------ */\n/* Helpers                                                             */\n/* ------------------------------------------------------------------ */\n\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst dayNum = (d: Date) => d.getFullYear() * 10000 + d.getMonth() * 100 + d.getDate();\nconst cmpDay = (a: Date, b: Date) => dayNum(a) - dayNum(b);\nconst diffDays = (a: Date, b: Date) =>\n  Math.round((Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()) - Date.UTC(a.getFullYear(), a.getMonth(), a.getDate())) / 864e5);\nconst pad = (n: number) => String(n).padStart(2, \"0\");\nconst dayKey = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\nconst isAllDay = (e: AgendaEvent) => !!e.allDay || e.end.getTime() - e.start.getTime() >= 864e5;\nconst lastDay = (e: AgendaEvent) => {\n  const last = startOfDay(new Date(Math.max(e.end.getTime() - 1, e.start.getTime())));\n  return cmpDay(last, e.start) < 0 ? startOfDay(e.start) : last;\n};\nconst duration = (mins: number) => (mins < 60 ? `${mins}m` : mins % 60 ? `${Math.floor(mins / 60)}h ${mins % 60}m` : `${mins / 60}h`);\n\nfunction subscribeLanguage(notify: () => void) {\n  window.addEventListener(\"languagechange\", notify);\n  return () => window.removeEventListener(\"languagechange\", notify);\n}\nconst useLocale = (locale?: string) => {\n  const browser = useSyncExternalStore(subscribeLanguage, () => navigator.language, () => \"en-US\");\n  return locale ?? browser;\n};\n\n/**\n * A clock that ticks every `ms`, aligned to the boundary, and resyncs when the tab\n * comes back. Null during the server render so nothing time-relative is prerendered.\n */\nexport function useClock(ms: number, override?: Date) {\n  const subscribe = useCallback(\n    (notify: () => void) => {\n      let timer = 0;\n      const arm = () => {\n        timer = window.setTimeout(() => (notify(), arm()), ms - (Date.now() % ms) + 10);\n      };\n      arm();\n      const onVisible = () => document.visibilityState === \"visible\" && notify();\n      document.addEventListener(\"visibilitychange\", onVisible);\n      return () => {\n        window.clearTimeout(timer);\n        document.removeEventListener(\"visibilitychange\", onVisible);\n      };\n    },\n    [ms],\n  );\n  const t = useSyncExternalStore(subscribe, () => Math.floor(Date.now() / ms) * ms, () => null);\n  return useMemo(() => override ?? (t === null ? null : new Date(t)), [t, override]);\n}\n\n/* ------------------------------------------------------------------ */\n/* Grouping                                                            */\n/* ------------------------------------------------------------------ */\n\ntype Row = { event: AgendaEvent; key: string; allDay: boolean; dayIndex: number; dayCount: number };\ntype Group = { date: Date; key: string; rows: Row[] };\n\n/** Events from today on, one group per day. Multi-day events appear on each day they cover. */\nexport function groupByDay(events: AgendaEvent[], today: Date, days: number): Group[] {\n  const groups = new Map<string, Group>();\n  const ensure = (d: Date) => {\n    const k = dayKey(d);\n    let g = groups.get(k);\n    if (!g) groups.set(k, (g = { date: d, key: k, rows: [] }));\n    return g;\n  };\n  ensure(today);\n  const horizon = addDays(today, days - 1);\n  for (const ev of events) {\n    const allDay = isAllDay(ev);\n    const first = startOfDay(ev.start);\n    const last = allDay ? lastDay(ev) : first;\n    const count = diffDays(first, last) + 1;\n    for (let i = 0; i < count; i++) {\n      const d = addDays(first, i);\n      if (cmpDay(d, today) < 0 || cmpDay(d, horizon) > 0) continue;\n      ensure(d).rows.push({ event: ev, key: count > 1 ? `${ev.id}@${dayKey(d)}` : ev.id, allDay, dayIndex: i + 1, dayCount: count });\n    }\n  }\n  return [...groups.values()]\n    .sort((a, b) => cmpDay(a.date, b.date))\n    .map((g) => ({\n      ...g,\n      rows: g.rows.sort((a, b) => Number(b.allDay) - Number(a.allDay) || a.event.start.getTime() - b.event.start.getTime() || a.event.end.getTime() - b.event.end.getTime()),\n    }));\n}\n\n/* ------------------------------------------------------------------ */\n/* Component                                                           */\n/* ------------------------------------------------------------------ */\n\nexport type AgendaProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"title\"> & {\n  events: AgendaEvent[];\n  /** Heading shown above the list, with the time beside it. */\n  title?: React.ReactNode;\n  /** How many days ahead to list, today included. */\n  days?: number;\n  /** Minutes before the start when Join becomes the primary action. */\n  joinLeadMinutes?: number;\n  /** Called when Join is pressed. Call event.preventDefault() to handle the link yourself. */\n  onJoin?: (event: AgendaEvent, e: React.MouseEvent<HTMLAnchorElement>) => void;\n  onEventClick?: (event: AgendaEvent) => void;\n  /** Shows a New event action in the empty state. */\n  onCreate?: () => void;\n  loading?: boolean;\n  locale?: string;\n  /** Pin the clock for tests and screenshots. */\n  now?: Date;\n  labels?: { today?: string; tomorrow?: string; now?: string; join?: string; joinNow?: string; empty?: string; nothingElse?: string; newEvent?: string };\n};\n\ntype JoinState = \"none\" | \"idle\" | \"soon\" | \"live\";\n\nexport function Agenda({\n  events,\n  title,\n  days = 14,\n  joinLeadMinutes = 5,\n  onJoin,\n  onEventClick,\n  onCreate,\n  loading = false,\n  locale: localeProp,\n  now: nowProp,\n  labels,\n  className,\n  ...rest\n}: AgendaProps) {\n  const L = {\n    today: \"Today\",\n    tomorrow: \"Tomorrow\",\n    now: \"Now\",\n    join: \"Join\",\n    joinNow: \"Join now\",\n    empty: \"No upcoming events\",\n    nothingElse: \"Nothing else today\",\n    newEvent: \"New event\",\n    ...labels,\n  };\n  const reduce = !!useReducedMotion();\n  const locale = useLocale(localeProp);\n  const lead = joinLeadMinutes * 60000;\n\n  // Tick every second only while a Join countdown is on screen; otherwise once a minute.\n  const minute = useClock(60000, nowProp);\n  const counting = useMemo(() => {\n    if (!minute) return false;\n    const t = minute.getTime();\n    return events.some((e) => e.meetingUrl && e.start.getTime() - t <= lead + 60000 && e.start.getTime() - t > -60000);\n  }, [events, minute, lead]);\n  const [visible, setVisible] = useState(true);\n  const second = useClock(counting && visible ? 1000 : 60000, nowProp);\n  const now = second ?? minute;\n  const today = useMemo(() => (now ? startOfDay(now) : null), [now]);\n  const nowMs = now?.getTime() ?? 0;\n\n  const fmt = useMemo(() => {\n    const make = (o: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat(locale, o);\n    return {\n      weekday: make({ weekday: \"long\" }),\n      date: make({ month: \"short\", day: \"numeric\" }),\n      time: make({ hour: \"numeric\", minute: \"2-digit\" }),\n      long: make({ weekday: \"long\", day: \"numeric\", month: \"long\" }),\n    };\n  }, [locale]);\n\n  const groups = useMemo(() => (today && !loading ? groupByDay(events, today, days) : null), [events, today, days, loading]);\n  const upcomingCount = useMemo(() => {\n    const ids = new Set<string>();\n    for (const g of groups ?? []) for (const r of g.rows) if (r.event.end.getTime() > nowMs && r.event.status !== \"canceled\") ids.add(r.event.id);\n    return ids.size;\n  }, [groups, nowMs]);\n\n  /* ---------- scrolling and the Now marker ---------- */\n  const rootRef = useRef<HTMLDivElement>(null);\n  const scrollerRef = useRef<HTMLDivElement>(null);\n  // State, not a ref: the marker remounts further down the list as events start, and the observer must follow it.\n  const [markerEl, setMarkerEl] = useState<HTMLLIElement | null>(null);\n  const [markerSide, setMarkerSide] = useState<\"above\" | \"below\" | null>(null);\n  const [showEarlier, setShowEarlier] = useState(false);\n\n  // Pause the per-second tick when the list is off screen.\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([e]) => setVisible(e.isIntersecting));\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  useEffect(() => {\n    const scroller = scrollerRef.current;\n    const marker = markerEl;\n    if (!scroller || !marker) return;\n    // Today is always the first group and finished events fold away, so the list opens at now by itself.\n    const io = new IntersectionObserver(\n      ([e]) => {\n        if (e.isIntersecting) return setMarkerSide(null);\n        setMarkerSide(e.boundingClientRect.top < (e.rootBounds?.top ?? 0) + 40 ? \"above\" : \"below\");\n      },\n      { root: scroller, rootMargin: \"-36px 0px 0px 0px\" },\n    );\n    io.observe(marker);\n    return () => io.disconnect();\n  }, [markerEl]);\n\n  const jumpToNow = () => {\n    const scroller = scrollerRef.current;\n    // Land on the call in progress if there is one, so it isn't hidden just above the marker.\n    const target = scroller?.querySelector<HTMLElement>(\"[data-live]\") ?? markerEl;\n    if (!scroller || !target) return;\n    scroller.scrollTo({ top: Math.max(0, target.offsetTop - 44), behavior: reduce ? \"auto\" : \"smooth\" });\n  };\n\n  /* ---------- keyboard: arrows and j/k between rows ---------- */\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    const keys = [\"ArrowDown\", \"ArrowUp\", \"j\", \"k\", \"Home\", \"End\"];\n    if (!keys.includes(e.key) || e.metaKey || e.ctrlKey || e.altKey) return;\n    // Folded-away earlier events are inert, so they're skipped.\n    const rows = [...(scrollerRef.current?.querySelectorAll<HTMLElement>(\"[data-agenda-row]\") ?? [])].filter((r) => !r.closest(\"[inert]\"));\n    const i = rows.indexOf(document.activeElement as HTMLElement);\n    if (i === -1 && !(e.target as HTMLElement).closest(\"[data-agenda-row], [data-join]\")) return;\n    e.preventDefault();\n    const from = i === -1 ? rows.indexOf((e.target as HTMLElement).closest(\"li\")?.querySelector<HTMLElement>(\"[data-agenda-row]\") ?? rows[0]) : i;\n    const next = e.key === \"Home\" ? 0 : e.key === \"End\" ? rows.length - 1 : Math.max(0, Math.min(rows.length - 1, from + (e.key === \"ArrowDown\" || e.key === \"j\" ? 1 : -1)));\n    rows[next]?.focus();\n    rows[next]?.scrollIntoView({ block: \"nearest\" });\n  };\n\n  const joinState = (e: AgendaEvent): JoinState => {\n    if (!e.meetingUrl || e.status === \"canceled\" || !now) return \"none\";\n    const s = e.start.getTime();\n    const end = e.end.getTime();\n    if (nowMs >= end) return \"none\";\n    if (nowMs >= s) return \"live\";\n    if (s - nowMs <= lead) return \"soon\";\n    return \"idle\";\n  };\n\n  const dayLabel = (d: Date) => {\n    if (!today) return \"\";\n    const diff = diffDays(today, d);\n    return diff === 0 ? L.today : diff === 1 ? L.tomorrow : fmt.weekday.format(d);\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"agenda\"\n      className={cn(\"relative flex min-h-[320px] min-w-0 flex-col overflow-hidden rounded-xl border border-line bg-raised shadow-[var(--shadow)]\", className)}\n      onKeyDown={onKeyDown}\n      {...rest}\n    >\n      {title && (\n        <div className=\"flex h-12 shrink-0 items-center justify-between gap-3 border-b border-line px-4\">\n          <h2 className=\"truncate text-[14px] font-medium tracking-[-0.015em] text-fg\">{title}</h2>\n          <span className=\"shrink-0 text-[12px] text-fg-3 tabular\" suppressHydrationWarning>\n            {now ? `${upcomingCount === 0 ? \"Nothing\" : upcomingCount === 1 ? \"1 event\" : `${upcomingCount} events`} ahead` : \"\"}\n          </span>\n        </div>\n      )}\n\n      <div ref={scrollerRef} className=\"relative min-h-0 flex-1 overflow-y-auto overscroll-contain\" aria-busy={loading || !now || undefined}>\n        {!groups ? (\n          <Skeleton />\n        ) : upcomingCount === 0 && groups.every((g) => g.rows.length === 0) ? (\n          <div className=\"flex h-full min-h-[220px] flex-col items-center justify-center gap-3 px-6 text-center\">\n            <span className=\"grid size-9 place-items-center rounded-full bg-hover text-fg-3\">\n              <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n                <rect x=\"2.5\" y=\"3.25\" width=\"11\" height=\"10.25\" rx=\"1.75\" />\n                <path d=\"M2.5 6.5h11M5.5 2v2.5M10.5 2v2.5\" />\n              </svg>\n            </span>\n            <p className=\"text-[13px] text-fg-2\">{L.empty}</p>\n            {onCreate && (\n              <button type=\"button\" onClick={onCreate} className={cn(btn, \"gap-1.5 bg-fg px-3 text-frame hover:bg-fg/90\")}>\n                <Plus size={14} />\n                {L.newEvent}\n              </button>\n            )}\n          </div>\n        ) : (\n          groups.map((g) => {\n            const isToday = today && cmpDay(g.date, today) === 0;\n            const past = isToday ? g.rows.filter((r) => !r.allDay && r.event.end.getTime() <= nowMs) : [];\n            const current = isToday ? g.rows.filter((r) => r.allDay || r.event.end.getTime() > nowMs) : g.rows;\n            // The marker sits before the first event that hasn't started; in-progress events stay above it.\n            const markerAt = isToday ? current.findIndex((r) => !r.allDay && r.event.start.getTime() > nowMs) : -1;\n            const nextUp = markerAt >= 0 ? current[markerAt] : null;\n            if (!isToday && g.rows.length === 0) return null;\n            return (\n              <section key={g.key} aria-labelledby={`agenda-${g.key}`}>\n                <h3\n                  id={`agenda-${g.key}`}\n                  className=\"sticky top-0 z-[2] flex h-9 items-baseline gap-2 border-b border-line bg-raised px-4 pt-2.5\"\n                >\n                  <span className={cn(\"text-[12.5px] font-medium\", isToday ? \"text-fg\" : \"text-fg-2\")}>{dayLabel(g.date)}</span>\n                  <span className=\"text-[12px] text-fg-3 tabular\">{fmt.date.format(g.date)}</span>\n                </h3>\n                <ul className=\"flex flex-col py-1\">\n                  {past.length > 0 && (\n                    <li>\n                      <button\n                        type=\"button\"\n                        onClick={() => setShowEarlier((s) => !s)}\n                        aria-expanded={showEarlier}\n                        className=\"flex h-8 w-full items-center gap-1.5 px-4 text-left text-[12px] text-fg-3 outline-none transition-colors duration-150 hover:text-fg focus-visible:bg-hover\"\n                      >\n                        <ChevronDown size={14} className={cn(\"transition-transform duration-200 ease-out-expo\", !showEarlier && \"-rotate-90\")} />\n                        {showEarlier ? \"Hide\" : \"Show\"} {past.length === 1 ? \"1 earlier event\" : `${past.length} earlier events`}\n                      </button>\n                      <div className={cn(\"grid transition-[grid-template-rows] duration-250 ease-in-out-quart motion-reduce:transition-none\", showEarlier ? \"grid-rows-[1fr]\" : \"grid-rows-[0fr]\")}>\n                        <ul className=\"min-h-0 overflow-hidden\" inert={!showEarlier}>\n                          {past.map((r) => (\n                            <AgendaRow key={r.key} row={r} state=\"past\" join=\"none\" now={nowMs} fmt={fmt} L={L} reduce={reduce} onJoin={onJoin} onEventClick={onEventClick} />\n                          ))}\n                        </ul>\n                      </div>\n                    </li>\n                  )}\n                  {current.map((r, i) => {\n                    const live = !r.allDay && r.event.start.getTime() <= nowMs && r.event.end.getTime() > nowMs;\n                    return (\n                      <FragmentWithMarker\n                        key={r.key}\n                        marker={i === markerAt ? <NowMarker ref={setMarkerEl} label={L.now} time={now ? fmt.time.format(now) : \"\"} /> : null}\n                      >\n                        <AgendaRow\n                          row={r}\n                          state={live ? \"live\" : r === nextUp ? \"next\" : \"upcoming\"}\n                          join={joinState(r.event)}\n                          now={nowMs}\n                          fmt={fmt}\n                          L={L}\n                          reduce={reduce}\n                          onJoin={onJoin}\n                          onEventClick={onEventClick}\n                        />\n                      </FragmentWithMarker>\n                    );\n                  })}\n                  {isToday && markerAt === -1 && (\n                    <>\n                      <NowMarker ref={setMarkerEl} label={L.now} time={now ? fmt.time.format(now) : \"\"} />\n                      {!current.some((r) => !r.allDay && r.event.end.getTime() > nowMs) && <li className=\"px-4 pb-2 pl-[84px] text-[12.5px] text-fg-3\">{L.nothingElse}</li>}\n                    </>\n                  )}\n                </ul>\n              </section>\n            );\n          })\n        )}\n      </div>\n\n      {/* When Now scrolls out of view, a pill brings it back from the side it went. */}\n      <AnimatePresence>\n        {markerSide && (\n          <motion.button\n            key=\"jump\"\n            type=\"button\"\n            onClick={jumpToNow}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: markerSide === \"above\" ? -6 : 6, scale: 0.96 }}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={{ opacity: 0, transition: { duration: 0.12 } }}\n            transition={reduce ? { duration: 0.15 } : spring.pop}\n            className={cn(\n              \"absolute left-1/2 z-[3] -ml-9 flex h-7 w-[72px] items-center justify-center gap-1 rounded-full border border-line-2 bg-raised text-[12px] font-medium text-fg shadow-pop select-none\",\n              \"outline-none transition-[background-color,scale] duration-150 hover:bg-hover focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3 active:scale-[0.96]\",\n              markerSide === \"above\" ? (title ? \"top-[92px]\" : \"top-11\") : \"bottom-3\",\n            )}\n          >\n            {markerSide === \"above\" ? <ArrowUp size={13} /> : <ArrowDown size={13} />}\n            {L.now}\n          </motion.button>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction FragmentWithMarker({ marker, children }: { marker: React.ReactNode; children: React.ReactNode }) {\n  return (\n    <>\n      {marker}\n      {children}\n    </>\n  );\n}\n\nconst btn = cn(\n  \"inline-flex h-7 shrink-0 items-center justify-center rounded-md text-[12.5px] font-medium select-none\",\n  \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n  \"transition-[background-color,border-color,color,scale] duration-150 ease-out active:scale-[0.97] active:duration-75\",\n);\n\nfunction NowMarker({ ref, label, time }: { ref: React.Ref<HTMLLIElement>; label: string; time: string }) {\n  return (\n    <li ref={ref} aria-label={`${label}, ${time}`} className=\"relative flex h-6 scroll-mt-10 items-center gap-2 pl-4 pr-4\">\n      <span className=\"w-[56px] shrink-0 font-mono text-2xs uppercase tracking-[0.08em] text-fg\">{label}</span>\n      <span aria-hidden className=\"relative h-px flex-1 bg-fg/70\">\n        <span className=\"absolute -left-[3px] -top-[3px] size-[7px] rounded-full bg-fg\" />\n      </span>\n      <span className=\"shrink-0 text-[11px] text-fg-2 tabular\" suppressHydrationWarning>\n        {time}\n      </span>\n    </li>\n  );\n}\n\ntype RowState = \"past\" | \"live\" | \"next\" | \"upcoming\";\ntype Fmt = { time: Intl.DateTimeFormat; long: Intl.DateTimeFormat };\n\nconst toneDot: Record<EventTone, string> = { neutral: \"bg-fg-3\", info: \"bg-info\", success: \"bg-success\", warning: \"bg-warning\", danger: \"bg-danger\" };\n\nfunction AgendaRow({\n  row,\n  state,\n  join,\n  now,\n  fmt,\n  L,\n  reduce,\n  onJoin,\n  onEventClick,\n}: {\n  row: Row;\n  state: RowState;\n  join: JoinState;\n  now: number;\n  fmt: Fmt;\n  L: { join: string; joinNow: string };\n  reduce: boolean;\n  onJoin?: AgendaProps[\"onJoin\"];\n  onEventClick?: AgendaProps[\"onEventClick\"];\n}) {\n  const e = row.event;\n  const tone = e.tone ?? \"neutral\";\n  const mins = Math.round((e.end.getTime() - e.start.getTime()) / 60000);\n  const tentative = e.status === \"tentative\";\n  const canceled = e.status === \"canceled\";\n  const startsIn = Math.max(0, e.start.getTime() - now);\n  const endsIn = Math.max(0, e.end.getTime() - now);\n  const progress = state === \"live\" ? Math.min(1, (now - e.start.getTime()) / Math.max(1, e.end.getTime() - e.start.getTime())) : 0;\n  const rel =\n    state === \"live\"\n      ? `Ends in ${duration(Math.max(1, Math.ceil(endsIn / 60000)))}`\n      : state === \"next\" && startsIn < 864e5 && join !== \"soon\"\n        ? startsIn < 60000\n          ? \"Starting now\"\n          : `In ${duration(Math.ceil(startsIn / 60000))}`\n        : \"\";\n  const meta = [row.allDay && row.dayCount > 1 ? `Day ${row.dayIndex} of ${row.dayCount}` : \"\", e.location, e.detail, tentative ? \"Tentative\" : canceled ? \"Canceled\" : \"\"].filter(Boolean).join(\" · \");\n  const label = `${e.title}, ${row.allDay ? \"all day\" : fmt.time.formatRange(e.start, e.end)}${meta ? `, ${meta}` : \"\"}${rel ? `, ${rel.toLowerCase()}` : \"\"}`;\n\n  return (\n    <li data-live={state === \"live\" || undefined} className={cn(\"group/row relative isolate flex items-start gap-3 px-4 py-2\", state === \"past\" && \"opacity-55\")}>\n      <div className=\"w-[56px] shrink-0 pt-px tabular\">\n        {row.allDay ? (\n          <span className=\"text-[12px] text-fg-3\">All day</span>\n        ) : (\n          <>\n            <span className={cn(\"block text-[12px]\", state === \"live\" ? \"font-medium text-fg\" : \"text-fg-2\")}>{fmt.time.format(e.start)}</span>\n            <span className=\"block text-[11px] text-fg-4\">{duration(mins)}</span>\n          </>\n        )}\n      </div>\n      <span aria-hidden className={cn(\"mt-[3px] w-[3px] shrink-0 self-stretch rounded-full\", tentative ? \"border border-dashed border-fg-4\" : toneDot[tone], canceled && \"opacity-40\")} />\n      <div className=\"min-w-0 flex-1\">\n        <button\n          type=\"button\"\n          data-agenda-row\n          aria-label={label}\n          onClick={() => onEventClick?.(e)}\n          className={cn(\n            \"line-clamp-2 max-w-full text-left text-[13px] font-medium leading-[18px] tracking-[-0.005em] text-fg outline-none [overflow-wrap:anywhere]\",\n            // The whole row is the target; the Join button sits above it.\n            \"after:absolute after:inset-x-1.5 after:inset-y-0.5 after:-z-10 after:rounded-lg after:transition-colors after:duration-150 hover:after:bg-fg/[0.035] focus-visible:after:bg-hover focus-visible:after:shadow-[inset_0_0_0_1px_var(--line-2)] active:after:bg-fg/[0.06]\",\n            canceled && \"text-fg-3 line-through decoration-fg-4\",\n          )}\n        >\n          {e.title}\n        </button>\n        {(meta || rel) && (\n          <p className=\"mt-0.5 flex min-w-0 items-center gap-1.5 text-[12px] leading-4 text-fg-3\">\n            {rel && (\n              <span className={cn(\"shrink-0 tabular\", state === \"live\" ? \"text-fg\" : \"text-fg-2\")} suppressHydrationWarning>\n                {rel}\n              </span>\n            )}\n            {rel && meta && <span className=\"text-fg-4\">·</span>}\n            {meta && <span className=\"truncate\">{meta}</span>}\n          </p>\n        )}\n        {state === \"live\" && (\n          <span aria-hidden className=\"mt-2 block h-[2px] overflow-hidden rounded-full bg-fg/10\">\n            <span className=\"block h-full origin-left rounded-full bg-fg/60 transition-transform duration-1000 ease-linear motion-reduce:transition-none\" style={{ transform: `scaleX(${progress})` }} />\n          </span>\n        )}\n      </div>\n      {join !== \"none\" && <JoinButton event={e} state={join} startsIn={startsIn} reduce={reduce} L={L} onJoin={onJoin} />}\n    </li>\n  );\n}\n\n/**\n * Quiet until it matters: a camera icon while the call is far off. Five minutes out\n * it grows into a filled Join with a countdown; once the call starts it says Join now\n * with a live dot. The width change springs, so the row never jumps.\n */\nfunction JoinButton({\n  event,\n  state,\n  startsIn,\n  reduce,\n  L,\n  onJoin,\n}: {\n  event: AgendaEvent;\n  state: Exclude<JoinState, \"none\">;\n  startsIn: number;\n  reduce: boolean;\n  L: { join: string; joinNow: string };\n  onJoin?: AgendaProps[\"onJoin\"];\n}) {\n  const prominent = state !== \"idle\";\n  const secs = Math.ceil(startsIn / 1000);\n  const t = reduce ? { duration: 0 } : spring.snappy;\n  return (\n    <motion.a\n      data-join\n      href={event.meetingUrl}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      aria-label={`${state === \"live\" ? L.joinNow : L.join} ${event.title}${state === \"soon\" ? `, starts in ${Math.floor(secs / 60)} minutes ${secs % 60} seconds` : \"\"}`}\n      title={state === \"idle\" ? `${L.join} call` : undefined}\n      onClick={(e) => onJoin?.(event, e)}\n      layout\n      // One nudge as it turns prominent: something changed while you weren't looking.\n      animate={prominent && !reduce ? { scale: [1, 1.06, 1] } : { scale: 1 }}\n      whileTap={reduce ? undefined : { scale: 0.95 }}\n      transition={{ layout: t, scale: { duration: 0.42, ease: ease.out } }}\n      style={{ borderRadius: 7 }}\n      data-state={state}\n      className={cn(\n        \"relative z-[1] mt-px inline-flex h-7 shrink-0 items-center justify-center gap-1.5 overflow-hidden border text-[12px] font-medium whitespace-nowrap select-none\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"transition-[background-color,border-color,color] duration-300 ease-out\",\n        \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n        prominent ? \"border-transparent bg-fg px-2.5 text-frame hover:bg-fg/90\" : \"w-7 border-line-2 text-fg-3 hover:border-fg-4 hover:bg-hover hover:text-fg\",\n      )}\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {state === \"idle\" ? (\n          <motion.svg\n            key=\"icon\"\n            layout=\"position\"\n            width=\"14\"\n            height=\"14\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.4\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            aria-hidden\n            initial={{ opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.6, transition: { duration: 0.1 } }}\n            transition={t}\n          >\n            <rect x=\"1.75\" y=\"4\" width=\"9\" height=\"8\" rx=\"1.75\" />\n            <path d=\"m10.75 7 3.5-2v6l-3.5-2\" />\n          </motion.svg>\n        ) : (\n          <motion.span\n            key=\"label\"\n            layout=\"position\"\n            className=\"flex items-center gap-1.5\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, filter: \"blur(2px)\" }}\n            animate={{ opacity: 1, filter: \"blur(0px)\" }}\n            exit={{ opacity: 0, transition: { duration: 0.1 } }}\n            transition={{ duration: 0.2, ease: ease.out }}\n          >\n            {state === \"live\" && (\n              <span aria-hidden className=\"relative grid size-1.5 place-items-center\">\n                <span className=\"absolute inset-0 rounded-full bg-success animate-ping-soft motion-reduce:animate-none\" />\n                <span className=\"relative size-1.5 rounded-full bg-success\" />\n              </span>\n            )}\n            {state === \"live\" ? L.joinNow : L.join}\n            {state === \"soon\" && (\n              <span aria-hidden className=\"flex items-center font-normal opacity-70 tabular\">\n                <NumberFlowGroup>\n                  <NumberFlow value={Math.floor(secs / 60)} animated={!reduce} />\n                  <span>:</span>\n                  <NumberFlow value={secs % 60} format={{ minimumIntegerDigits: 2 }} animated={!reduce} trend={-1} />\n                </NumberFlowGroup>\n              </span>\n            )}\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </motion.a>\n  );\n}\n\nfunction Skeleton() {\n  return (\n    <div aria-hidden className=\"flex flex-col\">\n      {[3, 2].map((n, g) => (\n        <div key={g}>\n          <div className=\"flex h-9 items-end border-b border-line px-4 pb-2.5\">\n            <span className=\"h-2.5 w-24 rounded bg-fg/[0.07]\" />\n          </div>\n          <div className=\"flex flex-col py-1\">\n            {Array.from({ length: n }, (_, i) => (\n              <div key={i} className=\"flex items-start gap-3 px-4 py-2 animate-pulse-soft motion-reduce:animate-none\">\n                <span className=\"mt-1 h-2.5 w-11 rounded bg-fg/[0.07]\" />\n                <span className=\"h-8 w-[3px] rounded-full bg-fg/[0.07]\" />\n                <span className=\"flex flex-1 flex-col gap-2 pt-1\">\n                  <span className=\"h-2.5 rounded bg-fg/[0.08]\" style={{ width: `${[62, 48, 70, 55, 40][(g * 3 + i) % 5]}%` }} />\n                  <span className=\"h-2 w-1/3 rounded bg-fg/[0.05]\" />\n                </span>\n              </div>\n            ))}\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/agenda"
}
