{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "time-ago",
  "title": "Time Ago",
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/time-ago.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState, useSyncExternalStore } from \"react\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// Pin timezone so server and client produce the same string during hydration.\nconst absoluteFmt = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"short\",\n  day: \"numeric\",\n  timeZone: \"UTC\",\n});\n\nconst MIN = 60;\nconst HOUR = 60 * 60;\nconst DAY = 24 * 60 * 60;\nconst WEEK = 7 * DAY;\nconst MONTH = 30.4375 * DAY;\nconst YEAR = 365.25 * DAY;\n\nfunction formatTimeAgo(dateStr: string): string {\n  const deltaMs = new Date(dateStr).getTime() - Date.now();\n  const future = deltaMs > 0;\n  const seconds = Math.abs(deltaMs) / 1000;\n\n  if (seconds < MIN) return future ? \"Soon\" : \"Just now\";\n\n  let value: string;\n  if (seconds < HOUR) value = `${Math.floor(seconds / MIN)}m`;\n  else if (seconds < DAY) value = `${Math.floor(seconds / HOUR)}h`;\n  else if (seconds < WEEK) value = `${Math.floor(seconds / DAY)}d`;\n  else if (seconds < MONTH) value = `${Math.floor(seconds / WEEK)}w`;\n  else if (seconds < YEAR) value = `${Math.floor(seconds / MONTH)}mo`;\n  else value = `${Math.floor(seconds / YEAR)}y`;\n\n  return future ? `in ${value}` : `${value} ago`;\n}\n\n// Returns null once the label rolls over slowly enough that scheduling refreshes would just churn.\nfunction nextRefreshDelay(dateStr: string): number | null {\n  const seconds = Math.abs((Date.now() - new Date(dateStr).getTime()) / 1000);\n  if (seconds < MIN) return 10_000;\n  if (seconds < HOUR) return 30_000;\n  if (seconds < DAY) return 60_000;\n  if (seconds < WEEK) return 60 * 60 * 1000;\n  return null;\n}\n\n// useSyncExternalStore as a \"has hydrated\" probe: false via getServerSnapshot, true post-mount.\nconst subscribe = () => () => {};\nconst getSnapshot = () => true;\nconst getServerSnapshot = () => false;\n\nexport interface TimeAgoProps\n  extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, \"dateTime\"> {\n  /** ISO date string. Past or future. */\n  dateStr: string;\n  /** Fallback rendered during SSR / initial hydration. Defaults to formatted absolute date. */\n  fallback?: string;\n  /** \"mono\" applies tabular numerals with tightened tracking; \"sans\" inherits parent typography. */\n  variant?: \"mono\" | \"sans\";\n  /** Re-render on a smart interval so the label stays current. */\n  liveUpdate?: boolean;\n}\n\n/** Renders an absolute date during SSR and swaps to a relative label after mount. */\nexport function TimeAgo({\n  dateStr,\n  fallback,\n  variant = \"mono\",\n  liveUpdate = true,\n  className,\n  ...props\n}: TimeAgoProps): React.ReactElement {\n  const mounted = useSyncExternalStore(\n    subscribe,\n    getSnapshot,\n    getServerSnapshot,\n  );\n\n  // `tick` in the effect's deps lets it schedule the next refresh based on the now-current age.\n  const [tick, setTick] = useState(0);\n\n  useEffect(() => {\n    if (!liveUpdate || !mounted) return;\n    const delay = nextRefreshDelay(dateStr);\n    if (delay == null) return;\n    const id = window.setTimeout(() => setTick((t) => t + 1), delay);\n    return () => window.clearTimeout(id);\n  }, [liveUpdate, mounted, dateStr, tick]);\n\n  const absolute = absoluteFmt.format(new Date(dateStr));\n  return (\n    <time\n      dateTime={dateStr}\n      data-slot=\"time-ago\"\n      className={cn(\n        variant === \"mono\" && \"tabular-nums tracking-[-0.01em]\",\n        className,\n      )}\n      {...props}\n    >\n      {mounted ? formatTimeAgo(dateStr) : (fallback ?? absolute)}\n    </time>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/time-ago.tsx"
    }
  ],
  "type": "registry:ui"
}