1import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
2
3import { observeResize } from "~/lib/observeResize";
4import { cn } from "~/lib/utils";
5
6export function AnimatedWidth({
7 children,
8 className,
9}: {
10 readonly children: ReactNode;
11 readonly className?: string;
12}) {
13 const contentRef = useRef<HTMLSpanElement>(null);
14 const [width, setWidth] = useState<number | null>(null);
15
16 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- observes the content element's width before paint to animate width
17 useLayoutEffect(() => {
18 const element = contentRef.current;
19 if (!element) return;
20 return observeResize(element, (entries) => {
21 const nextWidth = Math.ceil(entries.at(-1)?.contentRect.width ?? 0);
22 if (nextWidth > 0) setWidth(nextWidth);
23 });
24 }, []);
25
26 return (
27 <span
28 data-slot="animated-width"
29 className={cn(
30 "inline-block overflow-hidden transition-[width] duration-150 ease-out motion-reduce:transition-none",
31 className,
32 )}
33 style={width === null ? undefined : { width }}
34 >
35 <span ref={contentRef} className="block w-max whitespace-nowrap">
36 {children}
37 </span>
38 </span>
39 );
40}