{"version":3,"file":"Timestamp.mjs","sources":["../../src/primitives/Timestamp.tsx"],"sourcesContent":["\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport type { ReactNode } from \"react\";\nimport React, { forwardRef, useMemo } from \"react\";\n\nimport type { ComponentPropsWithSlot } from \"../types\";\nimport { capitalize } from \"../utils/capitalize\";\nimport { dateTimeFormat, relativeTimeFormat } from \"../utils/intl\";\nimport { useInterval } from \"../utils/use-interval\";\nimport { useRerender } from \"../utils/use-rerender\";\n\nconst DYNAMIC_DATE_THRESHOLD = 3 * 24 * 60 * 60 * 1000; // 3 days\nconst RENDER_INTERVAL = 30 * 1000; // 30 seconds\n\nconst TIMESTAMP_NAME = \"Timestamp\";\n\nexport interface TimestampProps\n  extends Omit<ComponentPropsWithSlot<\"time\">, \"children\" | \"title\"> {\n  /**\n   * The date to display.\n   */\n  date: Date | string | number;\n\n  /**\n   * A function to format the displayed date.\n   */\n  children?: (date: Date, locale?: string) => ReactNode;\n\n  /**\n   * The `title` attribute's value or a function to format it.\n   */\n  title?: string | ((date: Date, locale?: string) => string);\n\n  /**\n   * The interval in milliseconds at which the component will re-render.\n   * Can be set to `false` to disable re-rendering.\n   */\n  interval?: number | false;\n\n  /**\n   * The locale used when formatting the date.\n   */\n  locale?: string;\n}\n\nconst relativeUnits = {\n  seconds: 60,\n  minutes: 60,\n  hours: 24,\n  days: 7,\n  weeks: 4.34524,\n  months: 12,\n};\n\n/**\n * Formats a date absolutely.\n */\nfunction formatVerboseDate(date: Date, locale?: string) {\n  const formatter = dateTimeFormat(locale, {\n    year: \"numeric\",\n    month: \"numeric\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"numeric\",\n  });\n\n  return capitalize(formatter.format(date));\n}\n\n/**\n * Formats a date absolutely with only the day and month.\n */\nfunction formatShortDate(date: Date, locale?: string) {\n  const formatter = dateTimeFormat(locale, {\n    month: \"short\",\n    day: \"numeric\",\n  });\n\n  return capitalize(formatter.format(date));\n}\n\n// Some locales' relative formatting can be broken (e.g. \"-1h\") when using the narrow style.\nconst localesWithBrokenNarrowRelativeFormatting = [\n  \"br\",\n  \"fr\",\n  \"nb\",\n  \"nn\",\n  \"no\",\n  \"ro\",\n  \"sv\",\n];\n\n/**\n * Formats a date relatively.\n */\nfunction formatRelativeDate(date: Date, locale?: string) {\n  let resolvedLocale: string;\n\n  if (locale) {\n    resolvedLocale = locale;\n  } else {\n    const formatter = relativeTimeFormat();\n\n    resolvedLocale = formatter.resolvedOptions().locale;\n  }\n\n  const isBrokenWhenNarrow = localesWithBrokenNarrowRelativeFormatting.some(\n    (locale) =>\n      resolvedLocale === locale || resolvedLocale.startsWith(`${locale}-`)\n  );\n\n  const formatter = relativeTimeFormat(resolvedLocale, {\n    style: isBrokenWhenNarrow ? \"short\" : \"narrow\",\n    numeric: \"auto\",\n  });\n\n  let difference = (date.getTime() - Date.now()) / 1000;\n\n  if (\n    difference > -relativeUnits.seconds &&\n    difference < relativeUnits.seconds\n  ) {\n    return formatter.format(0, \"seconds\");\n  }\n\n  for (const [unit, length] of Object.entries(relativeUnits)) {\n    if (Math.abs(difference) < length) {\n      return formatter.format(\n        Math.round(difference),\n        unit as Intl.RelativeTimeFormatUnit\n      );\n    }\n\n    difference /= length;\n  }\n\n  return capitalize(formatter.format(Math.round(difference), \"years\"));\n}\n\n/**\n * Formats a date relatively if it's recent,\n * otherwise absolutely with only the day and month.\n */\nfunction formatDynamicDate(date: Date, locale?: string) {\n  return date.getTime() > Date.now() - DYNAMIC_DATE_THRESHOLD\n    ? formatRelativeDate(date, locale)\n    : formatShortDate(date, locale);\n}\n\n/**\n * Displays a formatted date, and automatically re-renders to support relative\n * formatting. Defaults to relative formatting for recent dates and a short\n * absolute formatting for older ones.\n *\n * @example\n * <Timestamp date={new Date()} />\n *\n * @example\n * <Timestamp date={new Date()} title={(date) => date.toISOString()} interval={false}>\n *   {(date) => date.toLocaleDateString()}\n * </Timestamp>\n */\nexport const Timestamp = forwardRef<HTMLTimeElement, TimestampProps>(\n  (\n    {\n      date,\n      locale,\n      children: renderChildren = formatDynamicDate,\n      title: renderTitle = formatVerboseDate,\n      dateTime,\n      interval = RENDER_INTERVAL,\n      asChild,\n      ...props\n    },\n    forwardedRef\n  ) => {\n    const Component = asChild ? Slot : \"time\";\n    const [rerender, key] = useRerender();\n    const parsedDate = useMemo(() => new Date(date), [date]);\n    const normalizedDate = useMemo(\n      () => parsedDate.toISOString(),\n      [parsedDate]\n    );\n    const title = useMemo(\n      () =>\n        typeof renderTitle === \"function\"\n          ? renderTitle(parsedDate, locale)\n          : renderTitle,\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n      [renderTitle, parsedDate, key]\n    );\n    const children = useMemo(\n      () =>\n        typeof renderChildren === \"function\"\n          ? renderChildren(parsedDate, locale)\n          : renderChildren,\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n      [renderChildren, parsedDate, key]\n    );\n\n    useInterval(rerender, interval);\n\n    return (\n      <Component\n        {...props}\n        ref={forwardedRef}\n        dateTime={dateTime ?? normalizedDate}\n        title={title}\n      >\n        {children}\n      </Component>\n    );\n  }\n);\n\nif (process.env.NODE_ENV !== \"production\") {\n  Timestamp.displayName = TIMESTAMP_NAME;\n}\n"],"names":[],"mappings":";;;;;;;;AAYA;AACA;AAEA;AA+BA;AAAsB;AACX;AACA;AACF;AACD;AACC;AAET;AAKA;AACE;AAAyC;AACjC;AACC;AACF;AACC;AACE;AAGV;AACF;AAKA;AACE;AAAyC;AAChC;AACF;AAGP;AACF;AAGA;AAAkD;AAChD;AACA;AACA;AACA;AACA;AACA;AAEF;AAKA;AACE;AAEA;AACE;AAAiB;AAEjB;AAEA;AAA6C;AAG/C;AAAqE;AAEE;AAGvE;AAAqD;AACb;AAC7B;AAGX;AAEA;AAIE;AAAoC;AAGtC;AACE;AACE;AAAiB;AACM;AACrB;AACF;AAGF;AAAc;AAGhB;AACF;AAMA;AACE;AAGF;AAeO;AAAkB;AAErB;AACE;AACA;AAC2B;AACN;AACrB;AACW;AACX;AACG;AAIL;AACA;AACA;AACA;AAAuB;AACQ;AAClB;AAEb;AAAc;AAIN;AAEuB;AAE/B;AAAiB;AAIT;AAE0B;AAGlC;AAEA;AACG;AACK;AACC;AACiB;AACtB;AAGF;AAGN;AAEA;AACE;AACF;;"}