1import { createContext, useContext, useEffect, useRef, useState } from "react";
2import { type PanelAnimationDurationMs } from "@t3tools/contracts/settings";
3
4import { useMediaQuery } from "./hooks/useMediaQuery";
5import { useClientSettings } from "./hooks/useSettings";
6
7const PanelAnimationSuppressionContext = createContext(false);
8
9export const PanelAnimationSuppressionProvider = PanelAnimationSuppressionContext.Provider;
10
11/**
12 * Suppresses panel motion for the first painted frame of an initial route or navigation.
13 * State restored by a route must be visible immediately; later user actions can animate.
14 */
15export function usePanelNavigationSuppression(navigationKey: string): boolean {
16 const [paintedNavigationKey, setPaintedNavigationKey] = useState<string | null>(null);
17 const suppressed = paintedNavigationKey !== navigationKey;
18
19 useEffect(() => {
20 if (!suppressed) return;
21 let releaseFrame = 0;
22 const paintFrame = window.requestAnimationFrame(() => {
23 releaseFrame = window.requestAnimationFrame(() => setPaintedNavigationKey(navigationKey));
24 });
25 return () => {
26 window.cancelAnimationFrame(paintFrame);
27 window.cancelAnimationFrame(releaseFrame);
28 };
29 }, [navigationKey, suppressed]);
30
31 return suppressed;
32}
33
34export function usePanelAnimationSettings(): {
35 active: boolean;
36 durationMs: PanelAnimationDurationMs;
37} {
38 const durationMs = useClientSettings((settings) => settings.panelAnimationDurationMs);
39 const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
40 const suppressed = useContext(PanelAnimationSuppressionContext);
41 return { active: durationMs > 0 && !prefersReducedMotion && !suppressed, durationMs };
42}
43
44/** Keeps closing panel content mounted until its opt-in transition ends. */
45export function usePanelPresence<T>(
46 open: boolean,
47 value: T | null,
48 animated: boolean,
49 scopeKey: string | null,
50 durationMs: PanelAnimationDurationMs,
51): { present: boolean; value: T | null } {
52 const [present, setPresent] = useState(open);
53 const retainedRef = useRef<{ scopeKey: string | null; value: T | null } | null>(
54 open ? { scopeKey, value } : null,
55 );
56
57 useEffect(() => {
58 if (open) retainedRef.current = { scopeKey, value };
59 }, [open, scopeKey, value]);
60
61 useEffect(() => {
62 if (open) {
63 setPresent(true);
64 return;
65 }
66 if (!animated) {
67 setPresent(false);
68 return;
69 }
70
71 const timeout = window.setTimeout(() => setPresent(false), durationMs);
72 return () => window.clearTimeout(timeout);
73 }, [animated, durationMs, open]);
74
75 const retainedValue =
76 retainedRef.current?.scopeKey === scopeKey ? retainedRef.current.value : null;
77 const visible = open || (animated && present && retainedRef.current?.scopeKey === scopeKey);
78 return { present: visible, value: open ? value : visible ? retainedValue : null };
79}