1import { useIsFocused } from "@react-navigation/native";
2import { useEffect, useLayoutEffect, useState } from "react";
3import { AppState } from "react-native";
4
5/**
6 * Wall clock for elapsed-time labels, advanced once a second only while
7 * `enabled`, the screen is focused, and the app is active. Retained routes stay
8 * mounted when hidden, so an ungated interval would keep re-rendering them.
9 * The clock re-reads the time before paint when ticking resumes, so a label
10 * never shows the moment it paused.
11 */
12export function useVisibleSecondClock(enabled: boolean): number {
13 const focused = useIsFocused();
14 const [appActive, setAppActive] = useState(() => AppState.currentState === "active");
15 const [nowMs, setNowMs] = useState(() => Date.now());
16 useEffect(() => {
17 const subscription = AppState.addEventListener("change", (state) =>
18 setAppActive(state === "active"),
19 );
20 // A change between the first render and this subscription has no event to replay.
21 // oxlint-disable-next-line react/set-state-in-effect -- Syncs state the listener missed.
22 setAppActive(AppState.currentState === "active");
23 return () => subscription.remove();
24 }, []);
25 const ticking = enabled && focused && appActive;
26 useLayoutEffect(() => {
27 if (!ticking) return;
28 // oxlint-disable-next-line react/set-state-in-effect -- Resuming reads the clock that stood still.
29 setNowMs(Date.now());
30 const intervalId = setInterval(() => setNowMs(Date.now()), 1_000);
31 return () => clearInterval(intervalId);
32 }, [ticking]);
33 return nowMs;
34}