{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "relative-time",
  "title": "Relative Time",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/relative-time/RelativeTime.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type {\n    RelativeTimeDisplay,\n    RelativeTimeNumeric,\n    RelativeTimeParseAs,\n    RelativeTimeStyle,\n  } from './format-relative-time'\n\n  // Omit `children`: the snippet carries label/absolute strings, which narrows the base `Snippet` type.\n  export interface RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>, 'children'> {\n    /** Instant to display. Accepts a Date, ISO string, or epoch ms. */\n    date: Date | string | number\n    /** Clock used for the delta. Pass in tests and SSR to keep output stable. */\n    now?: Date | string | number\n    /** Intl relative style. Named `formatStyle` so it does not collide with the HTML style attribute. */\n    formatStyle?: RelativeTimeStyle\n    /** `auto` yields \"yesterday\"; `always` yields \"1 day ago\". */\n    numeric?: RelativeTimeNumeric\n    /** BCP 47 locale. Defaults to the runtime locale. */\n    locale?: string\n    /** Visible label: relative (default), absolute clock, or both. */\n    display?: RelativeTimeDisplay\n    /** IANA zone for absolute text and the title tooltip. Omit for the browser local zone. Pass `UTC` for UTC. */\n    timeZone?: string\n    /** How to parse date strings with no offset. `local` is JS default; `utc` treats naive ISO as UTC. */\n    parseAs?: RelativeTimeParseAs\n    /** Tick interval in ms. `0` freezes the clock. */\n    updateInterval?: number\n    /** Override the rendered label. Receives the computed `label` and `absolute` strings. */\n    children?: Snippet<[{ label: string; absolute: string }]>\n    ref?: HTMLTimeElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import {\n    formatAbsoluteTime,\n    formatVisibleTime,\n    toDate,\n  } from './format-relative-time'\n\n  let {\n    class: className,\n    date,\n    now = undefined,\n    formatStyle = 'long',\n    numeric = 'auto',\n    locale = undefined,\n    display = 'relative',\n    timeZone = undefined,\n    parseAs = 'local',\n    updateInterval = 30_000,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: RelativeTimeProps = $props()\n\n  let tick = $state(0)\n\n  $effect(() => {\n    if (updateInterval <= 0 || now !== undefined) return\n    const timer = setInterval(() => {\n      tick += 1\n    }, updateInterval)\n    return () => clearInterval(timer)\n  })\n\n  const resolvedNow = $derived.by(() => {\n    tick\n    return now === undefined ? new Date() : toDate(now, parseAs)\n  })\n\n  const resolvedDate = $derived(toDate(date, parseAs))\n\n  const label = $derived(\n    formatVisibleTime(resolvedDate, resolvedNow, {\n      display,\n      style: formatStyle,\n      numeric,\n      locale,\n      timeZone,\n    }),\n  )\n\n  const absolute = $derived(formatAbsoluteTime(resolvedDate, locale, timeZone))\n  const iso = $derived(resolvedDate.toISOString())\n</script>\n\n<time\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"relative-time\"\n  datetime={iso}\n  title={absolute}\n  data-display={display}\n  data-timezone={timeZone || 'local'}\n  data-parse-as={parseAs}\n  class={cn('text-muted-foreground text-sm tabular-nums', className)}\n  {...restProps}\n>\n  {#if children}\n    {@render children({ label, absolute })}\n  {:else}\n    {label}\n  {/if}\n</time>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/relative-time/RelativeTime.svelte"
    },
    {
      "path": "packages/registry-svelte/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:ui",
      "target": "~/src/lib/components/ui/relative-time/format-relative-time.ts"
    },
    {
      "path": "packages/registry-svelte/components/relative-time/index.ts",
      "content": "export { default as RelativeTime, type RelativeTimeProps } from './RelativeTime.svelte'\nexport {\n  formatAbsoluteTime,\n  formatRelativeTime,\n  formatVisibleTime,\n  toDate,\n  type RelativeTimeDisplay,\n  type RelativeTimeNumeric,\n  type RelativeTimeParseAs,\n  type RelativeTimeStyle,\n} from './format-relative-time'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/relative-time/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "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"
  ]
}