{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "relative-time",
  "title": "Relative Time",
  "type": "registry:ui",
  "description": "Live relative timestamp — \"2 minutes ago\", \"yesterday\", \"in 3 days\" — via Intl.RelativeTimeFormat. `display` switches the label to relative, absolute, or both. `timeZone` (IANA or `UTC`) formats the clock and tooltip; omit it for the browser local zone. `parseAs` treats naive ISO strings as local or UTC. Renders a semantic <time>. Pass `now` to freeze the clock for tests and SSR.",
  "categories": [
    "display",
    "utility"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/relative-time/format-relative-time.ts",
      "content": "export type RelativeTimeStyle = 'long' | 'short' | 'narrow'\nexport type RelativeTimeNumeric = 'always' | 'auto'\nexport type RelativeTimeParseAs = 'local' | 'utc'\nexport type RelativeTimeDisplay = 'relative' | 'absolute' | 'both'\n\nconst DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [\n  { amount: 60, unit: 'second' },\n  { amount: 60, unit: 'minute' },\n  { amount: 24, unit: 'hour' },\n  { amount: 7, unit: 'day' },\n  { amount: 4.34524, unit: 'week' },\n  { amount: 12, unit: 'month' },\n  { amount: Number.POSITIVE_INFINITY, unit: 'year' },\n]\n\nconst NAIVE_ISO = /^(\\d{4}-\\d{2}-\\d{2})(?:T(\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,3})?)?))?$/\n\nexport function toDate(value: Date | string | number, parseAs: RelativeTimeParseAs = 'local'): Date {\n  if (value instanceof Date) return value\n  if (typeof value === 'number') return new Date(value)\n  if (parseAs === 'utc') {\n    const naive = value.match(NAIVE_ISO)\n    if (naive) return new Date(`${naive[1]}T${naive[2] ?? '00:00:00.000'}Z`)\n  }\n  return new Date(value)\n}\n\nexport function formatRelativeTime(\n  date: Date,\n  now: Date,\n  options?: { style?: RelativeTimeStyle; numeric?: RelativeTimeNumeric; locale?: string },\n): string {\n  const rtf = new Intl.RelativeTimeFormat(options?.locale, {\n    numeric: options?.numeric ?? 'auto',\n    style: options?.style ?? 'long',\n  })\n  let duration = (date.getTime() - now.getTime()) / 1000\n  for (const division of DIVISIONS) {\n    if (Math.abs(duration) < division.amount) {\n      return rtf.format(Math.round(duration), division.unit)\n    }\n    duration /= division.amount\n  }\n  return rtf.format(0, 'second')\n}\n\nexport function formatAbsoluteTime(date: Date, locale?: string, timeZone?: string): string {\n  return new Intl.DateTimeFormat(locale, {\n    dateStyle: 'medium',\n    timeStyle: 'short',\n    timeZone,\n  }).format(date)\n}\n\nexport function formatVisibleTime(\n  date: Date,\n  now: Date,\n  options?: {\n    display?: RelativeTimeDisplay\n    style?: RelativeTimeStyle\n    numeric?: RelativeTimeNumeric\n    locale?: string\n    timeZone?: string\n  },\n): string {\n  const relative = formatRelativeTime(date, now, options)\n  if ((options?.display ?? 'relative') === 'relative') return relative\n  const absolute = formatAbsoluteTime(date, options?.locale, options?.timeZone)\n  if (options?.display === 'absolute') return absolute\n  return `${relative} · ${absolute}`\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/relative-time/format-relative-time.ts"
    },
    {
      "path": "packages/elements-lit/src/components/relative-time/relative-time.ts",
      "content": "import { LitElement, css, html, isServer, nothing } from 'lit'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport {\n  formatAbsoluteTime,\n  formatVisibleTime,\n  toDate,\n  type RelativeTimeDisplay,\n  type RelativeTimeNumeric,\n  type RelativeTimeParseAs,\n  type RelativeTimeStyle,\n} from './format-relative-time'\n\ntype DateInput = Date | string | number\n\n// Attributes are strings; an all-digit string is epoch ms (React accepts numbers).\nconst fromAttr = (v: string | null) => (v == null ? undefined : /^-?\\d+$/.test(v) ? Number(v) : v)\n\n/**\n * <uip-relative-time> — the registry RelativeTime: a <time> showing \"5 minutes\n * ago\" (or absolute / both), with the full localized datetime as its title.\n *\n * Ticks every `update-interval` ms (default 30 000; `0` freezes) unless `now`\n * is given. The timer only runs in the browser while connected, so SSR output\n * is a single stable render. Slotted content replaces the label (React's\n * `children`).\n *\n * React's `className` → style the part from outside, e.g.\n * `class=\"[&::part(base)]:text-xs\"`.\n */\nexport class UipRelativeTime extends LitElement {\n  // display: contents so the inner <time> is laid out where React's is (inline\n  // in text, a flex item in a flex row). An inline host that gets blockified\n  // as a flex item adds its own line box (inherited line-height) around the\n  // smaller <time> text and grows the row.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    date: { converter: { fromAttribute: fromAttr } },\n    now: { converter: { fromAttribute: fromAttr } },\n    formatStyle: { attribute: 'format-style' },\n    numeric: {},\n    locale: {},\n    display: {},\n    timeZone: { attribute: 'time-zone' },\n    parseAs: { attribute: 'parse-as' },\n    updateInterval: { type: Number, attribute: 'update-interval' },\n  }\n\n  date?: DateInput\n  now?: DateInput\n  formatStyle: RelativeTimeStyle = 'long'\n  numeric: RelativeTimeNumeric = 'auto'\n  locale?: string\n  display: RelativeTimeDisplay = 'relative'\n  timeZone?: string\n  parseAs: RelativeTimeParseAs = 'local'\n  updateInterval = 30_000\n  private timer?: ReturnType<typeof setInterval>\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'relative-time')\n    this.syncTimer()\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.stopTimer()\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    if (changed.has('updateInterval') || changed.has('now')) this.syncTimer()\n  }\n\n  private stopTimer() {\n    clearInterval(this.timer)\n    this.timer = undefined\n  }\n\n  private syncTimer() {\n    this.stopTimer()\n    if (isServer || !this.isConnected || this.updateInterval <= 0 || this.now !== undefined) return\n    this.timer = setInterval(() => this.requestUpdate(), this.updateInterval)\n  }\n\n  render() {\n    if (this.date === undefined || this.date === '') return nothing\n    const resolvedDate = toDate(this.date, this.parseAs)\n    if (Number.isNaN(resolvedDate.getTime())) return nothing\n    const resolvedNow = this.now === undefined ? new Date() : toDate(this.now, this.parseAs)\n    const label = formatVisibleTime(resolvedDate, resolvedNow, {\n      display: this.display,\n      style: this.formatStyle,\n      numeric: this.numeric,\n      locale: this.locale,\n      timeZone: this.timeZone,\n    })\n    const absolute = formatAbsoluteTime(resolvedDate, this.locale, this.timeZone)\n    return html`<time\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"relative-time\"\n      data-display=${this.display}\n      data-timezone=${this.timeZone || 'local'}\n      data-parse-as=${this.parseAs}\n      datetime=${resolvedDate.toISOString()}\n      title=${absolute}\n      class=\"text-muted-foreground text-sm tabular-nums\"\n      ><slot>${label}</slot></time\n    >`\n  }\n}\n\ncustomElements.get('uip-relative-time') || customElements.define('uip-relative-time', UipRelativeTime)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-relative-time': UipRelativeTime\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/relative-time/relative-time.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
