1export const DRAFT_HERO_TRANSITION_ANIMATION_ID = "t3-draft-hero-transition";
2export const DRAFT_HERO_TRANSITION_EASING = "cubic-bezier(0.4, 0, 0.2, 1)";
3export const MOBILE_COMPOSER_VIEW_TRANSITION_NAME = "t3-mobile-composer";
4export const MOBILE_DRAFT_HEADLINE_VIEW_TRANSITION_NAME = "t3-mobile-draft-headline";
5const MOBILE_COMPOSER_TRANSITION_DURATION_PROPERTY = "--mobile-composer-transition-duration";
6
7type ComposerViewTransition = {
8 readonly finished: Promise<void>;
9};
10
11let activeMobileComposerTransition: Promise<void> | null = null;
12
13type ComposerViewTransitionDocument = Document & {
14 startViewTransition?: (update: () => void | Promise<void>) => ComposerViewTransition;
15};
16
17export async function waitForDraftHeroTransition(): Promise<void> {
18 const mobileComposerTransition = activeMobileComposerTransition;
19 if (typeof document === "undefined" || typeof document.getAnimations !== "function") {
20 await mobileComposerTransition;
21 return;
22 }
23
24 const activeTransitions = document
25 .getAnimations()
26 .filter((animation) => animation.id === DRAFT_HERO_TRANSITION_ANIMATION_ID);
27
28 await Promise.all([
29 mobileComposerTransition,
30 ...activeTransitions.map(async (animation) => {
31 try {
32 await animation.finished;
33 } catch {
34 // A cancelled transition is already safe to hand off.
35 }
36 }),
37 ]);
38}
39
40export async function runMobileComposerTransition(
41 update: () => void | Promise<void>,
42 options: { active: boolean; durationMs: number },
43): Promise<void> {
44 if (typeof document === "undefined" || typeof window === "undefined") {
45 await update();
46 return;
47 }
48
49 const transitionDocument = document as ComposerViewTransitionDocument;
50 const mobileViewport = window.matchMedia?.("(max-width: 639px)").matches ?? false;
51 const prefersReducedMotion =
52 window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
53 if (
54 !options.active ||
55 !mobileViewport ||
56 prefersReducedMotion ||
57 !transitionDocument.startViewTransition
58 ) {
59 await update();
60 return;
61 }
62
63 let updateStarted = false;
64 const runUpdate = async () => {
65 if (updateStarted) return;
66 updateStarted = true;
67 await update();
68 };
69 let transitionFinished: Promise<void> | null = null;
70 transitionDocument.documentElement.style.setProperty(
71 MOBILE_COMPOSER_TRANSITION_DURATION_PROPERTY,
72 `${String(options.durationMs)}ms`,
73 );
74 transitionDocument.documentElement.dataset.mobileComposerRouteTransition = "true";
75 try {
76 const transition = transitionDocument.startViewTransition(runUpdate);
77 transitionFinished = transition.finished.catch(() => undefined);
78 activeMobileComposerTransition = transitionFinished;
79 try {
80 await transition.finished;
81 } catch {
82 await runUpdate();
83 }
84 } catch {
85 await runUpdate();
86 } finally {
87 if (activeMobileComposerTransition === transitionFinished) {
88 activeMobileComposerTransition = null;
89 }
90 delete transitionDocument.documentElement.dataset.mobileComposerRouteTransition;
91 transitionDocument.documentElement.style.removeProperty(
92 MOBILE_COMPOSER_TRANSITION_DURATION_PROPERTY,
93 );
94 }
95}