import { RefObject } from 'react'; /** * How expensive the thing you are about to mount is. * * The heavier it is, the more free frame time the page has to show before it * is worth starting. Shared by {@link useSafeToMount} and `useSceneGate`. */ type MountCost = "light" | "normal" | "heavy"; /** * The thresholds each cost maps to. * * These replaced three separate numbers — minimum headroom, consecutive clean * frames, and a CPU core floor. Every one of them was a question a developer * had no way to answer, and the catalogue proved it: of ten call sites, six * passed the identical override and four passed nothing. That is not tuning, * it is people working around a default. "How expensive is this?" is a * question the person writing the component can actually answer. */ interface MountCostThresholds { /** Spare frame time a frame must show to count as clean, in ms. */ readonly headroomMs: number; /** How many clean frames in a row before the gate opens. */ readonly cleanFrames: number; /** Below this core count the gate never opens. */ readonly minCores: number; } /** * What each cost resolves to. Frozen, and readable for the same reason * `POINTER_INTENT_SENSITIVITY` is: a devtools panel or a docs demo * explaining why a gate is still closed needs the actual numbers, and the * alternative is every such surface keeping a copy that drifts. */ declare const SAFE_TO_MOUNT_COST: Readonly>; interface UseSafeToMountOptions { /** * How expensive the thing being mounted is. Default `"normal"`. * * - `"light"` — a small canvas, a handful of animated elements * - `"normal"` — most components * - `"heavy"` — a full 3D scene, post-processing, a large particle system */ cost?: MountCost; } /** * Returns `true` once the page has enough spare frame time to absorb an * expensive mount. * * ```tsx * const ready = useSafeToMount({ cost: "heavy" }); * return ready ? : ; * ``` * * It starts `false` and only ever flips to `true`. Something that unmounted * itself the moment it made the page slow would oscillate forever, so the gate * is one-way. * * On a machine with too few CPU cores for the given cost it stays `false` and * stops watching. Cores cannot improve while the page is open, so that is the * one condition that ends the story early. */ declare function useSafeToMount({ cost, }?: UseSafeToMountOptions): boolean; /** Where a heavy scene is in its life. */ type SceneState = /** Too far from the viewport to be worth existing. */ "dormant" /** Close enough to matter, waiting for the page to be able to afford it. */ | "warming" /** Running, full quality. */ | "active" /** Running, reduced quality — the device or the frame rate cannot take more. */ | "constrained" /** * Started, currently off screen. Still alive, drawing nothing. * * Scrolling past a scene must not destroy it. Rebuilding a WebGL context and * re-uploading its textures costs far more than leaving it mounted and * paused, so once a scene has started it never returns to `dormant`. */ | "idle" /** * The graphics context was taken away and a replacement is being built. * * Still `mounted` — the rebuild happens by remounting the canvas under a new * `generation`, so hiding it here would prevent the very thing that fixes it. */ | "recovering" /** Not running at all. Show a still image instead. */ | "poster"; interface UseSceneGateOptions { /** Shown in devtools. No effect on behaviour. */ label?: string; /** How expensive the scene is. Default `"heavy"`. */ cost?: MountCost; /** How far before the viewport to start warming, in px. Default 200. */ preload?: number; } /** * Why the gate is in its current state, in one machine-readable word. * * Branch on this. `reason` is the same fact written for a human and is not * stable enough to switch on, which up to 2.x the docs had to say out loud * because there was nothing else to offer. */ type SceneCause = "ok" | "not-near" | "waiting-for-headroom" | "off-screen" | "render-bound" | "frame-rate" | "device-floor" | "reduced-motion" | "context-lost"; interface SceneGate { /** * Attach to the element that holds the scene. * * **Destructure this hook's result.** `
` — reaching the * ref through a member expression — is a lint error under the React Compiler * rules, and it takes the rest of the object with it: `gate.generation` is a * number and gets reported as a ref read during render too. See hybrid-ref.ts * for why, and `compiler-lint.test.ts` for the check that keeps it true. */ ref: RefObject; state: SceneState; /** * Should the scene exist? True for `active`, `constrained` and `idle` — * `idle` included, because a scene that is merely off screen should be * paused, not torn down. */ mounted: boolean; /** * How much scene to build, or `null` when there is no scene. * * Up to 2.x this read `"reduced"` in `dormant` and `warming`, not because * quality was reduced but because nothing was running, and the docs carried * a rule telling you to check `mounted` first. A value that needs a rule to * read correctly is a defect, so it is `null` when it does not apply. */ quality: "full" | "reduced" | null; /** * Put this on the ``. It changes when the graphics context is * lost, which is what makes React throw away the dead tree and build a * working one — the rebuild React is already good at. */ generation: number; /** Machine-readable counterpart to `reason`. Branch on this one. */ cause: SceneCause; /** * Why it is in this state, in a sentence. Written for a support thread and a * devtools row. Branch on `cause` instead; this text is free to change. */ reason: string; } /** * Decide whether a heavy scene should exist, and how much of it. * * ```tsx * const { ref, mounted, quality } = useSceneGate({ label: "hero" }); * * return ( *
* {mounted ? ( * * * * ) : ( * * )} *
* ); * ``` * * Destructure it, as above. Holding the result as one object and writing * `
` is a lint error in any app running the React Compiler * rules — see `ref` on {@link SceneGate}. * * It answers three questions the page cannot answer for itself: is this scene * close enough to matter, can the page afford to start it, and once running, * is anything going wrong that reducing quality would actually fix. * * That last word matters. **Reduced quality is only a fix for rendering being * slow.** If a third-party script is blocking the main thread, halving the * particle count makes the page uglier and just as slow — so this gate reads * the pressure classifier and only responds to `"render"`. Nothing else * degrades the scene. * * ## What it does not do * * It decides; it does not apply. Nothing here touches a renderer, a device * pixel ratio or a post-processing pass — you own what `"full"` and * `"reduced"` look like. The renderer adapter that applies these decisions to * a real three.js scene is a separate piece. * * It composes `useSafeToMount` and `useAdaptiveQuality` rather than * reimplementing either, and both still work on their own. The pressure * classifier reaches it through `useAdaptiveQuality`, which since 3.0 fuses * that verdict into the tier. It replaced `useLazyScene` outright: that hook carried a second, * differently-behaved answer to "is the page healthy enough to mount", which * is the one shape this codebase cannot afford to keep duplicating. */ declare function useSceneGate({ label, cost, preload, }?: UseSceneGateOptions): SceneGate; export { type MountCost as M, type SceneState as S, type UseSafeToMountOptions as U, type MountCostThresholds as a, SAFE_TO_MOUNT_COST as b, type SceneGate as c, type UseSceneGateOptions as d, useSceneGate as e, useSafeToMount as u };