1import { useEffect, useRef, useState } from "react";
2
3import { cn } from "~/lib/utils";
4
5const HIDE_AFTER_MS = 1500;
6const ZOOM_EPSILON = 0.001;
7
8interface Props {
9 /** Current zoom factor (1.0 = 100%); changes drive the transient indicator. */
10 zoomFactor: number;
11}
12
13/**
14 * Floating "X%" pill that surfaces in the top-right of the webview area
15 * whenever the zoom factor changes, then fades out after a short pause.
16 *
17 * Suppressed for the first render's value so we don't flash 100% on mount.
18 */
19export function ZoomIndicator({ zoomFactor }: Props) {
20 const [visible, setVisible] = useState(false);
21 const lastFactorRef = useRef(zoomFactor);
22 const timerRef = useRef<number | null>(null);
23
24 useEffect(() => {
25 if (Math.abs(lastFactorRef.current - zoomFactor) < ZOOM_EPSILON) return;
26 lastFactorRef.current = zoomFactor;
27 setVisible(true);
28 if (timerRef.current !== null) window.clearTimeout(timerRef.current);
29 timerRef.current = window.setTimeout(() => {
30 setVisible(false);
31 timerRef.current = null;
32 }, HIDE_AFTER_MS);
33 return () => {
34 if (timerRef.current !== null) {
35 window.clearTimeout(timerRef.current);
36 timerRef.current = null;
37 }
38 };
39 }, [zoomFactor]);
40
41 const percent = `${Math.round(zoomFactor * 100)}%`;
42
43 return (
44 <div
45 aria-hidden={!visible}
46 className={cn(
47 "pointer-events-none absolute top-3 right-3 z-20 select-none rounded-full border border-border/70 bg-popover/95 px-2.5 py-1 text-xs font-medium text-foreground shadow-md/10 backdrop-blur transition-all duration-200 ease-out",
48 visible ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-1",
49 )}
50 >
51 {percent}
52 </div>
53 );
54}