import { useCallback, useEffect, useRef, useState } from "react";
/**
* Drop-in replacement for react-full-screen's useFullScreenHandle + FullScreen component.
*
* Motivation: react-full-screen 1.1.1 doesn't debounce fullscreenchange events, so on
* Windows systems with DPI scaling > 100%, the resize triggered during fullscreen entry
* fires a fullscreenchange event that the library misinterprets as an exit, immediately
* popping back out of fullscreen.
*
* This hook bypasses the library entirely and calls the native Fullscreen API directly,
* ignoring fullscreenchange events fired within DEBOUNCE_MS of entry to absorb the
* DPI-scaling resize blip.
*
* Usage — replace in BinVisualizerV2.tsx:
*
* // Remove:
* import { FullScreen, useFullScreenHandle } from "react-full-screen";
* const fullScreenHandler = useFullScreenHandle();
* ...
*
* // Add:
* import { useFullScreen } from "hooks/useFullScreen";
* const { fullScreenHandler, FullScreenWrapper } = useFullScreen();
* ...
*
* // Everything else (fullScreenHandler.active, .enter(), .exit()) stays the same.
*/
const DEBOUNCE_MS = 500;
export interface FullScreenHandle {
active: boolean;
enter: () => void;
exit: () => void;
}
export function useFullScreen(): {
fullScreenHandler: FullScreenHandle;
FullScreenWrapper: React.FC<{ children: React.ReactNode }>;
} {
const [active, setActive] = useState(false);
const containerRef = useRef(null);
// Timestamp of the last enter() call — used to debounce spurious exit events
const enterTimeRef = useRef(0);
const enter = useCallback(() => {
const el = containerRef.current;
if (!el) return;
enterTimeRef.current = Date.now();
if (el.requestFullscreen) {
el.requestFullscreen().catch(() => {
// Some browsers (e.g. iOS Safari) reject the promise — silently ignore
});
} else if ((el as any).webkitRequestFullscreen) {
(el as any).webkitRequestFullscreen();
} else if ((el as any).mozRequestFullScreen) {
(el as any).mozRequestFullScreen();
} else if ((el as any).msRequestFullscreen) {
(el as any).msRequestFullscreen();
}
}, []);
const exit = useCallback(() => {
if (document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
} else if ((document as any).webkitExitFullscreen) {
(document as any).webkitExitFullscreen();
} else if ((document as any).mozCancelFullScreen) {
(document as any).mozCancelFullScreen();
} else if ((document as any).msExitFullscreen) {
(document as any).msExitFullscreen();
}
}, []);
useEffect(() => {
const handleChange = () => {
const fullscreenEl =
document.fullscreenElement ||
(document as any).webkitFullscreenElement ||
(document as any).mozFullScreenElement ||
(document as any).msFullscreenElement;
const isNowFullscreen = fullscreenEl === containerRef.current;
// If we just called enter() and this event fires within DEBOUNCE_MS,
// and it looks like an exit, ignore it — it's the DPI-scaling resize blip
if (!isNowFullscreen && Date.now() - enterTimeRef.current < DEBOUNCE_MS) {
return;
}
setActive(isNowFullscreen);
};
document.addEventListener("fullscreenchange", handleChange);
document.addEventListener("webkitfullscreenchange", handleChange);
document.addEventListener("mozfullscreenchange", handleChange);
document.addEventListener("MSFullscreenChange", handleChange);
return () => {
document.removeEventListener("fullscreenchange", handleChange);
document.removeEventListener("webkitfullscreenchange", handleChange);
document.removeEventListener("mozfullscreenchange", handleChange);
document.removeEventListener("MSFullscreenChange", handleChange);
};
}, []);
const FullScreenWrapper: React.FC<{ children: React.ReactNode }> = useCallback(
({ children }) => (
{children}
),
[]
);
return {
fullScreenHandler: { active, enter, exit },
FullScreenWrapper,
};
}