{
  "$schema": "https://abackui.com/schema/registry-item.json",
  "name": "relative-time",
  "type": "registry:ui",
  "title": "Relative Time",
  "description": "A hydration-safe \"3 hours ago\" timestamp, localized by Intl and refreshed on one shared clock.",
  "registryDependencies": [
    "https://abackui.com/r/utils.json"
  ],
  "categories": [
    "Data Display"
  ],
  "docs": "https://abackui.com/docs/components/relative-time",
  "meta": {
    "status": "beta",
    "usedIn": [
      "aback-tools",
      "aback-launch"
    ]
  },
  "files": [
    {
      "path": "components/ui/relative-time.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nconst UNITS: ReadonlyArray<[Intl.RelativeTimeFormatUnit, number]> = [\n  [\"year\", 31_536_000],\n  [\"month\", 2_592_000],\n  [\"week\", 604_800],\n  [\"day\", 86_400],\n  [\"hour\", 3_600],\n  [\"minute\", 60],\n];\n\nfunction toDate(value: Date | string | number) {\n  return value instanceof Date ? value : new Date(value);\n}\n\n/**\n * \"3 hours ago\", \"yesterday\", \"in 2 days\", localized by Intl. Differences\n * under 45 seconds read as \"now\". Counts are truncated, so 2 h 50 min is\n * \"2 hours ago\", never a rounded-up time that has not happened yet.\n */\nfunction formatRelativeTime(\n  value: Date | string | number,\n  now: number = Date.now(),\n  locale?: string,\n) {\n  const date = toDate(value);\n  const seconds = (date.getTime() - now) / 1000;\n  const format = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n  if (Math.abs(seconds) < 45) return format.format(0, \"second\");\n  for (const [unit, size] of UNITS) {\n    if (Math.abs(seconds) >= size) return format.format(Math.trunc(seconds / size), unit);\n  }\n  return format.format(Math.trunc(seconds / 60) || (seconds < 0 ? -1 : 1), \"minute\");\n}\n\ninterface Clock {\n  now: number;\n  listeners: Set<() => void>;\n  timer?: ReturnType<typeof setInterval>;\n}\n\n// One shared clock per refresh interval, so a list of a hundred timestamps\n// re-renders on one timer instead of a hundred. Nothing runs until a\n// component subscribes, and the timer stops with the last subscriber.\nconst clocks = new Map<number, Clock>();\n\nfunction clockFor(interval: number) {\n  let clock = clocks.get(interval);\n  if (!clock) {\n    clock = { now: Date.now(), listeners: new Set() };\n    clocks.set(interval, clock);\n  }\n  return clock;\n}\n\nfunction subscribeClock(interval: number, listener: () => void) {\n  const clock = clockFor(interval);\n  clock.listeners.add(listener);\n  if (!clock.timer && interval > 0) {\n    clock.timer = setInterval(() => {\n      clock.now = Date.now();\n      clock.listeners.forEach((notify) => notify());\n    }, interval);\n  }\n  return () => {\n    clock.listeners.delete(listener);\n    if (clock.listeners.size === 0) {\n      if (clock.timer) clearInterval(clock.timer);\n      clocks.delete(interval);\n    }\n  };\n}\n\nexport interface RelativeTimeProps\n  extends Omit<React.ComponentProps<\"time\">, \"dateTime\" | \"children\"> {\n  /** The moment to describe. Accepts a Date, an ISO string, or a timestamp. */\n  date: Date | string | number;\n  /** BCP 47 locale. Defaults to the browser's language. */\n  locale?: string;\n  /** How often the text updates, in milliseconds. 0 renders once. */\n  refreshInterval?: number;\n  /** Format of the server render and the hover title. */\n  absoluteFormat?: Intl.DateTimeFormatOptions;\n}\n\nconst DEFAULT_ABSOLUTE: Intl.DateTimeFormatOptions = {\n  dateStyle: \"medium\",\n  timeStyle: \"short\",\n  timeZone: \"UTC\",\n};\n\n/**\n * A relative timestamp that is safe on cached and server-rendered pages. The\n * server and the first client render show the absolute date, so hydration\n * matches; the relative phrase replaces it after mount. The absolute date\n * stays available as the title and in the machine-readable `dateTime`.\n */\nfunction RelativeTime({\n  date,\n  locale,\n  refreshInterval = 60_000,\n  absoluteFormat = DEFAULT_ABSOLUTE,\n  title,\n  ...props\n}: RelativeTimeProps) {\n  const subscribe = React.useCallback(\n    (listener: () => void) => subscribeClock(refreshInterval, listener),\n    [refreshInterval],\n  );\n  const now = React.useSyncExternalStore<number | null>(\n    subscribe,\n    () => clockFor(refreshInterval).now,\n    () => null,\n  );\n\n  const value = toDate(date);\n  if (Number.isNaN(value.getTime())) return null;\n\n  const absolute = new Intl.DateTimeFormat(locale ?? \"en-US\", absoluteFormat).format(value);\n\n  return (\n    <time\n      data-slot=\"relative-time\"\n      dateTime={value.toISOString()}\n      title={title ?? absolute}\n      {...props}\n    >\n      {now === null ? absolute : formatRelativeTime(value, now, locale)}\n    </time>\n  );\n}\n\nexport { RelativeTime, formatRelativeTime };\n",
      "type": "registry:ui",
      "target": "components/ui/relative-time.tsx"
    }
  ]
}
