{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marker",
  "title": "Marker",
  "description": "Quiet notes between content, like date dividers and system events, with sticky dates and times that read as Today or Yesterday.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "class-variance-authority",
    "cn"
  ],
  "registryDependencies": [
    "https://hextaui.com/r/theme.json"
  ],
  "files": [
    {
      "path": "components/ui/marker.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\n\nconst markerVariants = cva(\n  \"group/marker relative flex min-h-4 w-full min-w-0 items-center gap-2 text-start text-sm text-muted-foreground [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:decoration-foreground/30 [&_a:not([data-slot])]:underline-offset-3 [&_a:not([data-slot])]:hover:text-foreground [&_a:not([data-slot])]:hover:decoration-foreground [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        separator:\n          \"before:h-(--hairline) before:min-w-4 before:flex-1 before:bg-border before:transition-opacity before:duration-200 after:h-(--hairline) after:min-w-4 after:flex-1 after:bg-border after:transition-opacity after:duration-200 data-stuck:before:opacity-0 data-stuck:after:opacity-0 motion-reduce:before:transition-none motion-reduce:after:transition-none\",\n        border: \"border-b border-border pb-2\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\ntype MarkerVariant = NonNullable<VariantProps<typeof markerVariants>[\"variant\"]>\n\nfunction scrollParent(element: HTMLElement) {\n  let node = element.parentElement\n  while (node && node !== document.body) {\n    const { overflowY } = getComputedStyle(node)\n    if (/(auto|scroll|overlay)/.test(overflowY)) {\n      return node\n    }\n    node = node.parentElement\n  }\n  return null\n}\n\nfunction useStuck(ref: React.RefObject<HTMLElement | null>, enabled: boolean) {\n  const [stuck, setStuck] = React.useState(false)\n\n  React.useEffect(() => {\n    const element = ref.current\n    if (!enabled || !element || typeof IntersectionObserver === \"undefined\") {\n      setStuck(false)\n      return\n    }\n\n    const offset = parseFloat(getComputedStyle(element).top) || 0\n    const root = scrollParent(element)\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        const top = entry.rootBounds?.top ?? 0\n        setStuck(\n          entry.intersectionRatio > 0 &&\n            entry.intersectionRatio < 1 &&\n            entry.boundingClientRect.top < top + offset + 1\n        )\n      },\n      {\n        root,\n        rootMargin: `${-(offset + 1)}px 0px 0px 0px`,\n        threshold: [0, 1],\n      }\n    )\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [ref, enabled])\n\n  return stuck\n}\n\ntype MarkerProps = useRender.ComponentProps<\"div\"> & {\n  variant?: MarkerVariant\n  sticky?: boolean\n}\n\nfunction Marker({\n  className,\n  variant = \"default\",\n  sticky = false,\n  render,\n  ref,\n  ...props\n}: MarkerProps) {\n  const markerRef = React.useRef<HTMLDivElement | null>(null)\n  const stuck = useStuck(markerRef, sticky)\n  const setRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      markerRef.current = node\n      if (typeof ref === \"function\") {\n        return ref(node)\n      }\n      if (ref) {\n        ref.current = node\n      }\n      return undefined\n    },\n    [ref]\n  )\n\n  return useRender({\n    defaultTagName: \"div\",\n    render,\n    ref: setRef,\n    props: mergeProps<\"div\">(\n      {\n        className: cn(\n          markerVariants({ variant }),\n          sticky &&\n            \"sticky top-[calc(var(--marker-sticky-top,--spacing(2))-1px)] z-10\",\n          className\n        ),\n      },\n      props,\n      {\n        \"data-slot\": \"marker\",\n        \"data-variant\": variant,\n        ...(sticky ? { \"data-sticky\": \"\" } : {}),\n        ...(stuck ? { \"data-stuck\": \"\" } : {}),\n      } as React.ComponentProps<\"div\">\n    ),\n  })\n}\n\nfunction MarkerIcon({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"marker-icon\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"inline-flex size-4 shrink-0 items-center justify-center [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction MarkerContent({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"marker-content\"\n      className={cn(\n        \"min-w-0 rounded-4xl text-pretty wrap-break-word transition-[background-color,box-shadow,padding,color] duration-200 ease-out-quint group-data-stuck/marker:bg-popover group-data-stuck/marker:px-2.5 group-data-stuck/marker:py-0.5 group-data-stuck/marker:text-foreground group-data-stuck/marker:shadow-sm group-data-stuck/marker:ring-(length:--hairline) group-data-stuck/marker:ring-foreground/10 group-data-[variant=separator]/marker:flex-initial group-data-[variant=separator]/marker:text-center motion-reduce:transition-none\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nconst dayMs = 24 * 60 * 60 * 1000\n\nfunction startOfDay(date: Date) {\n  return new Date(date.getFullYear(), date.getMonth(), date.getDate())\n}\n\nfunction toDate(value: Date | string | number) {\n  return value instanceof Date ? value : new Date(value)\n}\n\nfunction capitalize(text: string, locale: Intl.LocalesArgument) {\n  return (\n    text.charAt(0).toLocaleUpperCase(locale as string | undefined) +\n    text.slice(1)\n  )\n}\n\nfunction formatDay(\n  value: Date,\n  now: Date | null,\n  locale: Intl.LocalesArgument\n) {\n  if (Number.isNaN(value.getTime())) {\n    return \"\"\n  }\n  const sameYear = now ? value.getFullYear() === now.getFullYear() : false\n  const absolute = new Intl.DateTimeFormat(locale, {\n    month: \"short\",\n    day: \"numeric\",\n    ...(sameYear ? {} : { year: \"numeric\" }),\n  }).format(value)\n  if (!now) {\n    return absolute\n  }\n  const days = Math.round(\n    (startOfDay(now).getTime() - startOfDay(value).getTime()) / dayMs\n  )\n  if (days === 0 || days === 1) {\n    return capitalize(\n      new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" }).format(\n        -days,\n        \"day\"\n      ),\n      locale\n    )\n  }\n  if (days > 1 && days < 7) {\n    return new Intl.DateTimeFormat(locale, { weekday: \"long\" }).format(value)\n  }\n  return absolute\n}\n\nlet midnightListeners = new Set<() => void>()\nlet midnightTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction scheduleMidnight() {\n  clearTimeout(midnightTimer)\n  const now = new Date()\n  const next = startOfDay(new Date(now.getTime() + dayMs)).getTime()\n  midnightTimer = setTimeout(\n    () => {\n      for (const notify of midnightListeners) {\n        notify()\n      }\n      scheduleMidnight()\n    },\n    Math.max(1000, next - now.getTime() + 50)\n  )\n}\n\nfunction subscribeToday(notify: () => void) {\n  midnightListeners.add(notify)\n  if (midnightListeners.size === 1) {\n    scheduleMidnight()\n  }\n  return () => {\n    midnightListeners.delete(notify)\n    if (midnightListeners.size === 0) {\n      clearTimeout(midnightTimer)\n      midnightListeners = new Set()\n    }\n  }\n}\n\nfunction getToday() {\n  return startOfDay(new Date()).getTime()\n}\n\nfunction getServerToday() {\n  return null\n}\n\ntype MarkerTimeProps = Omit<React.ComponentProps<\"time\">, \"children\"> & {\n  date: Date | string | number\n  locale?: Intl.LocalesArgument\n  format?: (date: Date) => React.ReactNode\n}\n\nfunction MarkerTime({\n  className,\n  date,\n  locale = \"en-US\",\n  format,\n  ...props\n}: MarkerTimeProps) {\n  const today = React.useSyncExternalStore(\n    subscribeToday,\n    getToday,\n    getServerToday\n  )\n  const value = toDate(date)\n  const valid = !Number.isNaN(value.getTime())\n\n  return (\n    <time\n      data-slot=\"marker-time\"\n      dateTime={valid ? value.toISOString() : undefined}\n      suppressHydrationWarning\n      className={cn(\"tabular-nums\", className)}\n      {...props}\n    >\n      {format\n        ? valid\n          ? format(value)\n          : null\n        : formatDay(value, today === null ? null : new Date(today), locale)}\n    </time>\n  )\n}\n\nexport { Marker, MarkerContent, MarkerIcon, MarkerTime, markerVariants }\nexport type { MarkerProps, MarkerTimeProps, MarkerVariant }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "chat"
  ],
  "type": "registry:ui"
}