@getnarro/atlas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/check/index.d.ts +82 -0
  4. package/dist/check/index.js +150 -0
  5. package/dist/check/index.js.map +1 -0
  6. package/dist/chunk-5SRKWTOU.js +33 -0
  7. package/dist/chunk-5SRKWTOU.js.map +1 -0
  8. package/dist/chunk-BDF2X5LC.js +28 -0
  9. package/dist/chunk-BDF2X5LC.js.map +1 -0
  10. package/dist/chunk-RHTCDWZD.js +142 -0
  11. package/dist/chunk-RHTCDWZD.js.map +1 -0
  12. package/dist/chunk-RWYOYDBD.js +75 -0
  13. package/dist/chunk-RWYOYDBD.js.map +1 -0
  14. package/dist/chunk-SD4Y4EBG.js +69 -0
  15. package/dist/chunk-SD4Y4EBG.js.map +1 -0
  16. package/dist/chunk-UTCNNNPT.js +89 -0
  17. package/dist/chunk-UTCNNNPT.js.map +1 -0
  18. package/dist/embed/narro-atlas.js +12 -0
  19. package/dist/image/index.d.ts +132 -0
  20. package/dist/image/index.js +117 -0
  21. package/dist/image/index.js.map +1 -0
  22. package/dist/index.d.ts +33 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lod-DBuZ0MG3.d.ts +86 -0
  26. package/dist/map/index.d.ts +222 -0
  27. package/dist/map/index.js +184 -0
  28. package/dist/map/index.js.map +1 -0
  29. package/dist/path-BIxJH8g2.d.ts +101 -0
  30. package/dist/provider-CjL3i-0A.d.ts +54 -0
  31. package/dist/react/index.d.ts +183 -0
  32. package/dist/react/index.js +291 -0
  33. package/dist/react/index.js.map +1 -0
  34. package/dist/replay/index.d.ts +162 -0
  35. package/dist/replay/index.js +138 -0
  36. package/dist/replay/index.js.map +1 -0
  37. package/dist/tag-BP51cBza.d.ts +24 -0
  38. package/dist/timeline-B_dNydQA.d.ts +194 -0
  39. package/dist/video/index.d.ts +101 -0
  40. package/dist/video/index.js +91 -0
  41. package/dist/video/index.js.map +1 -0
  42. package/dist/view-BzztOpV6.d.ts +83 -0
  43. package/dist/web/embed.d.ts +59 -0
  44. package/dist/web/embed.js +192 -0
  45. package/dist/web/embed.js.map +1 -0
  46. package/dist/web/index.d.ts +89 -0
  47. package/dist/web/index.js +4 -0
  48. package/dist/web/index.js.map +1 -0
  49. package/package.json +119 -0
@@ -0,0 +1,101 @@
1
+ import { V as View } from './view-BzztOpV6.js';
2
+
3
+ /**
4
+ * The camera path: an ordered list of named framings, as data.
5
+ *
6
+ * A waypoint names a place, never an index. `TransformConfig.positions` is a
7
+ * `Map<string, SlidePosition>` keyed by slide index, and that is the second thing this
8
+ * package fixes: an index renumbers itself the moment somebody inserts a waypoint above
9
+ * it, and it cannot be a URL fragment, an export filename, or the thing a checklist item
10
+ * binds to. A name can be all four.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ /**
16
+ * Where a waypoint looks.
17
+ *
18
+ * Either a framing in world units, or a **reference** the scene provider resolves — a
19
+ * CSS selector over a replay, a node id on a plane. A reference is the authorable form:
20
+ * `"#publish-button"` survives a re-record that moves the button, where the coordinates
21
+ * it resolved to last week do not.
22
+ */
23
+ type WaypointTarget = View | string;
24
+ /** One named framing on the path. */
25
+ interface Waypoint {
26
+ /**
27
+ * A stable name, unique within the path.
28
+ *
29
+ * It is the URL fragment, the export filename and the binding a checklist item uses,
30
+ * so it outlives reordering in a way an index cannot.
31
+ */
32
+ id: string;
33
+ /** Where to look: a `{ cx, cy, w }` framing, or a reference for the provider. */
34
+ at: WaypointTarget;
35
+ /**
36
+ * How far the camera pulls back on the way *in* to this waypoint — van Wijk's ρ.
37
+ *
38
+ * @defaultValue {@link DEFAULT_RHO}
39
+ */
40
+ arc?: number;
41
+ /**
42
+ * How long the flight *in* to this waypoint takes, in **seconds**.
43
+ *
44
+ * Omit it and the flight uses its own `suggestedDuration`, which is derived from how
45
+ * far the picture actually travels. Overriding it is what `narro check` measures
46
+ * against that suggestion — an override far below it is the lurch, and far above it is
47
+ * the sixteen-minute camera move.
48
+ */
49
+ duration?: number;
50
+ /** How long to rest here before flying on, in seconds. Only autoplay and video use it. */
51
+ hold?: number;
52
+ /** Human-readable label, for a checklist, a progress rail or an export filename. */
53
+ label?: string;
54
+ /** A chapter in a `.narrocast`, for the replay provider. Ignored by other providers. */
55
+ chapter?: string;
56
+ }
57
+ /** A camera path over one scene. */
58
+ interface AtlasPath {
59
+ waypoints: Waypoint[];
60
+ /** ρ for any waypoint that does not set its own. */
61
+ arc?: number;
62
+ /** `hold` for any waypoint that does not set its own. Seconds. */
63
+ hold?: number;
64
+ }
65
+ /** A waypoint whose target has been resolved to a concrete framing. */
66
+ interface ResolvedWaypoint extends Omit<Waypoint, "at"> {
67
+ at: View;
68
+ /** The reference this came from, when it was authored as one. */
69
+ ref?: string;
70
+ /** Its position on the path. */
71
+ index: number;
72
+ }
73
+ /** Why a waypoint could not be resolved. */
74
+ interface UnresolvedWaypoint {
75
+ id: string;
76
+ index: number;
77
+ ref: string;
78
+ }
79
+ interface ResolveResult {
80
+ resolved: ResolvedWaypoint[];
81
+ unresolved: UnresolvedWaypoint[];
82
+ }
83
+ /** Is this target a concrete framing rather than a reference for the provider? */
84
+ declare function isView(target: WaypointTarget): target is View;
85
+ /**
86
+ * Turn every waypoint's target into a framing, using `resolve` for the references.
87
+ *
88
+ * Unresolved waypoints are returned rather than thrown: the checker wants to report all
89
+ * of them at once, and a host wants to skip the broken one and still fly the rest.
90
+ */
91
+ declare function resolvePath(path: AtlasPath, resolve: (ref: string) => View | null): ResolveResult;
92
+ /** The ρ a waypoint flies in with, falling back to the path's and then the default. */
93
+ declare function arcFor(path: AtlasPath, waypoint: Pick<Waypoint, "arc">): number;
94
+ /** The rest a waypoint takes, falling back to the path's and then to none. */
95
+ declare function holdFor(path: AtlasPath, waypoint: Pick<Waypoint, "hold">): number;
96
+ /** Find a waypoint by name. */
97
+ declare function waypointIndex(path: AtlasPath, id: string): number;
98
+ /** Names used more than once, in the order they first repeat. */
99
+ declare function duplicateIds(path: AtlasPath): string[];
100
+
101
+ export { type AtlasPath as A, type ResolvedWaypoint as R, type UnresolvedWaypoint as U, type Waypoint as W, arcFor as a, type ResolveResult as b, type WaypointTarget as c, duplicateIds as d, holdFor as h, isView as i, resolvePath as r, waypointIndex as w };
@@ -0,0 +1,54 @@
1
+ import { R as Rect, c as Viewport, V as View } from './view-BzztOpV6.js';
2
+
3
+ /**
4
+ * One interface, so a deck, a video and a web page render four different kinds of scene
5
+ * the same way.
6
+ *
7
+ * The member that earns the interface is {@link SceneProvider.ready}. A plane is ready
8
+ * the instant it mounts, but a map has to fetch tiles and a replay has to reach a
9
+ * chapter — both over the network, both asynchronously. The video renderer advances a
10
+ * frame counter as fast as it can, so without a gate it will happily render frame 12 of
11
+ * a map that has not drawn anything yet and encode a video of grey squares. That failure
12
+ * is the single most likely way a spatial canvas ships broken, so the check for it is in
13
+ * the type rather than in a provider's good intentions.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+
18
+ /** The providers this package knows about. A closed set, so `catalog.json` can carry it. */
19
+ declare const PROVIDER_NAMES: readonly ["plane", "replay", "map", "image", "dom"];
20
+ type ProviderName = (typeof PROVIDER_NAMES)[number];
21
+ interface SceneProvider {
22
+ /** Which kind of scene this is. */
23
+ readonly name: ProviderName;
24
+ /**
25
+ * The world rectangle the scene occupies, or null when it does not know yet.
26
+ *
27
+ * The checker uses it to catch a waypoint that flies to somewhere there is nothing.
28
+ */
29
+ bounds(): Rect | null;
30
+ /**
31
+ * A waypoint reference — a CSS selector, a node id — resolved to a framing.
32
+ *
33
+ * Null when it matches nothing, which is a diagnostic rather than a crash: the host
34
+ * skips that waypoint and the checker reports it by name.
35
+ */
36
+ resolve(ref: string, viewport: Viewport): View | null;
37
+ /**
38
+ * Has everything this view needs finished loading?
39
+ *
40
+ * The video host holds the frame until this is true; the deck host ignores it, because
41
+ * a presenter can see perfectly well that the tiles have not arrived.
42
+ */
43
+ ready(view: View): boolean;
44
+ /**
45
+ * Put the view on screen itself.
46
+ *
47
+ * Return `true` when the provider has handled it — a map moves its own camera — and
48
+ * the host will not apply a CSS transform. Return `false`, or leave it undefined, and
49
+ * the host applies the one composited transform that is the default rendering model.
50
+ */
51
+ apply?(view: View, viewport: Viewport): boolean;
52
+ }
53
+
54
+ export { PROVIDER_NAMES as P, type SceneProvider as S, type ProviderName as a };
@@ -0,0 +1,183 @@
1
+ import React from 'react';
2
+ import { A as AtlasPath } from '../path-BIxJH8g2.js';
3
+ import { c as Viewport, V as View } from '../view-BzztOpV6.js';
4
+ import { P as PlacedNode } from '../lod-DBuZ0MG3.js';
5
+ import { S as SceneProvider } from '../provider-CjL3i-0A.js';
6
+
7
+ /**
8
+ * The deck host: one plane, one camera, one composited transform.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ interface AtlasContextValue {
14
+ /** Where the camera is this frame. */
15
+ view: View;
16
+ /** Is a flight in progress? */
17
+ flying: boolean;
18
+ /** Index of the waypoint the camera is at or heading for. */
19
+ index: number;
20
+ /** Its id, when there is one. */
21
+ waypointId: string | null;
22
+ total: number;
23
+ goTo: (indexOrId: number | string) => void;
24
+ next: () => void;
25
+ previous: () => void;
26
+ }
27
+ /**
28
+ * Read the camera from inside a scene.
29
+ *
30
+ * A checklist beside the canvas ticking as the camera arrives is this hook plus the
31
+ * waypoint ids, and nothing else.
32
+ */
33
+ declare function useAtlas(): AtlasContextValue;
34
+ interface AtlasProps {
35
+ /** The camera path. */
36
+ path: AtlasPath;
37
+ /** The scene. Defaults to a plane built from `nodes`. */
38
+ provider?: SceneProvider;
39
+ /** Nodes on the default plane. Ignored when `provider` is given. */
40
+ nodes?: readonly PlacedNode[];
41
+ /** What to render on the plane. Node ids come from `data-atlas-id`. */
42
+ children?: React.ReactNode;
43
+ /**
44
+ * The coordinate grid the world is measured in.
45
+ *
46
+ * This is the *world*, not the surface it is painted on: it fixes what `at: 0, 0, 4000`
47
+ * means, and it is what a reference resolves against. The actual pixel size of the
48
+ * canvas is measured, so a deck rendered at any size frames the same content.
49
+ *
50
+ * @defaultValue 1920x1080
51
+ */
52
+ viewport?: Viewport;
53
+ /** Which waypoint to start at. @defaultValue 0 */
54
+ initialWaypoint?: number | string;
55
+ /**
56
+ * Take the arrow keys.
57
+ *
58
+ * Uses the same contract as `Slide`'s sub-slides: capture the key, and swallow it only
59
+ * while there is another waypoint to move to, so the deck advances normally at either
60
+ * end. Without that, the deck and the canvas both act on one press.
61
+ *
62
+ * @defaultValue true
63
+ */
64
+ keyboard?: boolean;
65
+ /** Only listen while this is true — a deck passes its slide's `isActive`. @defaultValue true */
66
+ active?: boolean;
67
+ /** Called on arrival at each waypoint. */
68
+ onArrive?: (id: string, index: number) => void;
69
+ className?: string;
70
+ style?: React.CSSProperties;
71
+ }
72
+ /**
73
+ * A zooming canvas.
74
+ *
75
+ * ```tsx
76
+ * <Atlas path={{ waypoints: [
77
+ * { id: "whole", at: { cx: 0, cy: 0, w: 4000 } },
78
+ * { id: "ingest", at: "#ingest" },
79
+ * ] }} nodes={nodes}>
80
+ * <div data-atlas-id="ingest">…</div>
81
+ * </Atlas>
82
+ * ```
83
+ *
84
+ * @behaviour Fills its container and clips to it, so it wants a slide of its own. Its
85
+ * children are placed in world units on a plane the camera moves over rather than laid out
86
+ * by the slide — `AtlasNode` is what places them — and a node the camera cannot see, or
87
+ * that its level-of-detail band excludes, is unmounted rather than merely hidden.
88
+ */
89
+ declare function Atlas({ path, provider, nodes, children, viewport, initialWaypoint, keyboard, active, onArrive, className, style, }: AtlasProps): React.ReactElement;
90
+ /**
91
+ * Is this node currently mounted?
92
+ *
93
+ * A node the scene never described — a backdrop, a caption, a progress rail — is always
94
+ * mounted. Culling is opt-in per node, so declaring `nodes` can never make content the
95
+ * author did not describe disappear.
96
+ */
97
+ declare function useIsMounted(id: string | undefined): boolean;
98
+ interface AtlasNodeProps {
99
+ /** Must match a `PlacedNode.id`, or the node is never culled. */
100
+ id: string;
101
+ /** Where it sits, in world units. */
102
+ x: number;
103
+ y: number;
104
+ width: number;
105
+ height: number;
106
+ children?: React.ReactNode;
107
+ className?: string;
108
+ style?: React.CSSProperties;
109
+ }
110
+ /**
111
+ * A node on the plane.
112
+ *
113
+ * `contain: layout paint` is not decoration: it stops a node's own layout from escaping
114
+ * into the camera root, which is what keeps a frame at one style recalculation and zero
115
+ * layout however many nodes are mounted.
116
+ *
117
+ * @behaviour Positions itself absolutely at `x`/`y` in world units, so it only works
118
+ * inside an `Atlas` and ignores whatever layout surrounds it. It renders nothing at all
119
+ * while the camera cannot see it, or while its `lod` band excludes the current framing.
120
+ */
121
+ declare function AtlasNode({ id, x, y, width, height, children, className, style, }: AtlasNodeProps): React.ReactElement | null;
122
+
123
+ /**
124
+ * The camera as a hook: a target waypoint goes in, a view comes out every frame.
125
+ *
126
+ * Driven by `requestAnimationFrame` rather than by a CSS transition or a spring library,
127
+ * for three reasons. The flight is not a cubic bézier, so a CSS transition cannot express
128
+ * it. The view is needed in JavaScript anyway, because culling and level-of-detail are
129
+ * decided from it. And the same `flight` has to serve the video host, where there is no
130
+ * rAF at all — so the interpolation lives in a pure function and only the clock differs.
131
+ *
132
+ * @packageDocumentation
133
+ */
134
+
135
+ /** Does this viewer want less motion? Re-read live, because they can change it mid-deck. */
136
+ declare function usePrefersReducedMotion(): boolean;
137
+ interface CameraState {
138
+ /** Where the camera is right now. */
139
+ view: View;
140
+ /** Is a flight in progress? The `will-change` switch keys off this. */
141
+ flying: boolean;
142
+ }
143
+ interface UseCameraOptions {
144
+ /** Where the camera should end up. */
145
+ target: View;
146
+ /** How long to take, in seconds. Omit and the flight's own suggestion is used. */
147
+ duration?: number;
148
+ /** van Wijk's ρ. */
149
+ arc?: number;
150
+ /** Cut instead of flying — what a reduced-motion viewer gets. */
151
+ cut?: boolean;
152
+ /** Called once the camera settles. */
153
+ onArrive?: () => void;
154
+ }
155
+ /**
156
+ * Fly the camera to `target` whenever it changes.
157
+ *
158
+ * A target that changes mid-flight is honoured immediately: the new flight starts from
159
+ * wherever the camera actually is, not from the waypoint it was heading for. A presenter
160
+ * who presses `→` twice quickly gets one continuous move, not a jump.
161
+ */
162
+ declare function useCamera({ target, duration, arc, cut, onArrive, }: UseCameraOptions): CameraState;
163
+ /**
164
+ * The `will-change` value for a camera root, and the reason it is not a constant.
165
+ *
166
+ * Chrome re-rasterises content when its transform scale changes — *unless* it carries
167
+ * `will-change: transform`, which is a promise to the compositor to apply the transform
168
+ * fast and skip the re-raster. That makes the property a straight trade:
169
+ *
170
+ * - always on → cheap frames, and permanently blurry after a zoom, because the texture
171
+ * is still rasterised at whatever scale the layer was created at. This is the blur
172
+ * reported against impress.js and against WebKit 3D transforms for a decade.
173
+ * - never set → sharp, and a re-raster of the whole subtree on every frame of a zoom.
174
+ *
175
+ * So switch it: on while flying, off on arrival. The blur then exists only during the
176
+ * move, when nobody is reading, and every resting frame — every frame anyone stops on,
177
+ * screenshots, or exports — is sharp.
178
+ */
179
+ declare function cameraWillChange(flying: boolean): "transform" | "auto";
180
+ /** Step to a waypoint index, clamped, without wrapping. */
181
+ declare function clampIndex(index: number, length: number): number;
182
+
183
+ export { Atlas, type AtlasContextValue, AtlasNode, type AtlasNodeProps, type AtlasProps, type CameraState, type UseCameraOptions, cameraWillChange, clampIndex, useAtlas, useCamera, useIsMounted, usePrefersReducedMotion };
@@ -0,0 +1,291 @@
1
+ import { planeProvider } from '../chunk-BDF2X5LC.js';
2
+ import { visibleNodes } from '../chunk-5SRKWTOU.js';
3
+ import { flight, resolvePath, arcFor } from '../chunk-RHTCDWZD.js';
4
+ import { transformToCss, viewToTransform } from '../chunk-RWYOYDBD.js';
5
+ import { createContext, useState, useEffect, useRef, useMemo, useContext } from 'react';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ function usePrefersReducedMotion() {
9
+ const [reduced, setReduced] = useState(() => {
10
+ if (typeof window === "undefined" || !window.matchMedia) return false;
11
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
12
+ });
13
+ useEffect(() => {
14
+ if (typeof window === "undefined" || !window.matchMedia) return;
15
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
16
+ const onChange = (event) => setReduced(event.matches);
17
+ query.addEventListener("change", onChange);
18
+ return () => query.removeEventListener("change", onChange);
19
+ }, []);
20
+ return reduced;
21
+ }
22
+ function useCamera({
23
+ target,
24
+ duration,
25
+ arc,
26
+ cut = false,
27
+ onArrive
28
+ }) {
29
+ const [view, setView] = useState(target);
30
+ const [flying, setFlying] = useState(false);
31
+ const viewRef = useRef(view);
32
+ viewRef.current = view;
33
+ const onArriveRef = useRef(onArrive);
34
+ onArriveRef.current = onArrive;
35
+ const { cx, cy, w } = target;
36
+ useEffect(() => {
37
+ const to = { cx, cy, w };
38
+ const from = viewRef.current;
39
+ if (from.cx === to.cx && from.cy === to.cy && from.w === to.w) {
40
+ return;
41
+ }
42
+ if (cut) {
43
+ setView(to);
44
+ setFlying(false);
45
+ onArriveRef.current?.();
46
+ return;
47
+ }
48
+ const path = flight(from, to, arc);
49
+ const seconds = duration ?? path.suggestedDuration;
50
+ if (seconds <= 0) {
51
+ setView(to);
52
+ onArriveRef.current?.();
53
+ return;
54
+ }
55
+ let raf = 0;
56
+ let start = null;
57
+ setFlying(true);
58
+ const step = (now) => {
59
+ if (start === null) start = now;
60
+ const t = Math.min(1, (now - start) / (seconds * 1e3));
61
+ setView(path.at(t));
62
+ if (t < 1) {
63
+ raf = requestAnimationFrame(step);
64
+ } else {
65
+ setFlying(false);
66
+ onArriveRef.current?.();
67
+ }
68
+ };
69
+ raf = requestAnimationFrame(step);
70
+ return () => {
71
+ cancelAnimationFrame(raf);
72
+ setFlying(false);
73
+ };
74
+ }, [cx, cy, w, cut, duration, arc]);
75
+ return useMemo(() => ({ view, flying }), [view, flying]);
76
+ }
77
+ function cameraWillChange(flying) {
78
+ return flying ? "transform" : "auto";
79
+ }
80
+ function clampIndex(index, length) {
81
+ if (length <= 0) return 0;
82
+ return Math.min(Math.max(index, 0), length - 1);
83
+ }
84
+ var DEFAULT_VIEWPORT = { width: 1920, height: 1080 };
85
+ var AtlasContext = createContext(null);
86
+ function useAtlas() {
87
+ const value = useContext(AtlasContext);
88
+ if (!value) throw new Error("useAtlas must be used inside an <Atlas>");
89
+ return value;
90
+ }
91
+ function Atlas({
92
+ path,
93
+ provider,
94
+ nodes = [],
95
+ children,
96
+ viewport = DEFAULT_VIEWPORT,
97
+ initialWaypoint = 0,
98
+ keyboard = true,
99
+ active = true,
100
+ onArrive,
101
+ className,
102
+ style
103
+ }) {
104
+ const scene = useMemo(() => provider ?? planeProvider({ nodes }), [provider, nodes]);
105
+ const { resolved } = useMemo(
106
+ () => resolvePath(path, (ref) => scene.resolve(ref, viewport)),
107
+ [path, scene, viewport]
108
+ );
109
+ const startIndex = useMemo(() => {
110
+ if (typeof initialWaypoint === "string") {
111
+ const found = resolved.findIndex((w) => w.id === initialWaypoint);
112
+ return found === -1 ? 0 : found;
113
+ }
114
+ return clampIndex(initialWaypoint, resolved.length);
115
+ }, [initialWaypoint, resolved]);
116
+ const rootRef = useRef(null);
117
+ const [painted, setPainted] = useState(null);
118
+ useEffect(() => {
119
+ const element = rootRef.current;
120
+ if (!element) return;
121
+ const measure = () => {
122
+ const width = element.offsetWidth;
123
+ const height = element.offsetHeight;
124
+ if (width <= 0 || height <= 0) return;
125
+ setPainted(
126
+ (previous) => previous && previous.width === width && previous.height === height ? previous : { width, height }
127
+ );
128
+ };
129
+ measure();
130
+ if (typeof ResizeObserver === "undefined") return;
131
+ const observer = new ResizeObserver(measure);
132
+ observer.observe(element);
133
+ return () => observer.disconnect();
134
+ }, []);
135
+ const surface = painted ?? viewport;
136
+ const [index, setIndex] = useState(startIndex);
137
+ const safeIndex = clampIndex(index, resolved.length);
138
+ const current = resolved[safeIndex];
139
+ const reduced = usePrefersReducedMotion();
140
+ const target = current?.at ?? { cx: 0, cy: 0, w: viewport.width };
141
+ const { view, flying } = useCamera({
142
+ target,
143
+ duration: current?.duration,
144
+ arc: current ? arcFor(path, current) : void 0,
145
+ cut: reduced,
146
+ onArrive: () => {
147
+ if (current) onArrive?.(current.id, safeIndex);
148
+ }
149
+ });
150
+ useEffect(() => {
151
+ if (!keyboard || !active || resolved.length === 0) return;
152
+ const onKeyDown = (event) => {
153
+ if (event.ctrlKey || event.metaKey) return;
154
+ const target2 = event.target;
155
+ if (target2 && (target2.tagName === "INPUT" || target2.tagName === "TEXTAREA" || target2.isContentEditable)) {
156
+ return;
157
+ }
158
+ const forward = event.key === "ArrowRight" || event.key === " " || event.key === "Enter";
159
+ const back = event.key === "ArrowLeft";
160
+ if (forward && safeIndex < resolved.length - 1) {
161
+ event.preventDefault();
162
+ event.stopPropagation();
163
+ setIndex(safeIndex + 1);
164
+ } else if (back && safeIndex > 0) {
165
+ event.preventDefault();
166
+ event.stopPropagation();
167
+ setIndex(safeIndex - 1);
168
+ } else if (event.key === "Home") {
169
+ event.preventDefault();
170
+ event.stopPropagation();
171
+ setIndex(0);
172
+ } else if (event.key === "End") {
173
+ event.preventDefault();
174
+ event.stopPropagation();
175
+ setIndex(resolved.length - 1);
176
+ }
177
+ };
178
+ window.addEventListener("keydown", onKeyDown, { capture: true });
179
+ return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
180
+ }, [keyboard, active, safeIndex, resolved.length]);
181
+ const keep = useMemo(() => {
182
+ const ids = /* @__PURE__ */ new Set();
183
+ for (const offset of [-1, 0, 1]) {
184
+ const w = resolved[safeIndex + offset];
185
+ if (w?.ref) ids.add(w.ref.startsWith("#") ? w.ref.slice(1) : w.ref);
186
+ }
187
+ return ids;
188
+ }, [resolved, safeIndex]);
189
+ const mounted = useMemo(
190
+ () => new Set(visibleNodes(nodes, view, viewport, { keep }).map((n) => n.id)),
191
+ [nodes, view, viewport, keep]
192
+ );
193
+ const handled = scene.apply?.(view, surface) ?? false;
194
+ const transform = handled ? void 0 : transformToCss(viewToTransform(view, surface));
195
+ const context = useMemo(
196
+ () => ({
197
+ view,
198
+ flying,
199
+ index: safeIndex,
200
+ waypointId: current?.id ?? null,
201
+ total: resolved.length,
202
+ goTo: (indexOrId) => {
203
+ const next = typeof indexOrId === "string" ? resolved.findIndex((w) => w.id === indexOrId) : indexOrId;
204
+ if (next >= 0) setIndex(clampIndex(next, resolved.length));
205
+ },
206
+ next: () => setIndex((i) => clampIndex(i + 1, resolved.length)),
207
+ previous: () => setIndex((i) => clampIndex(i - 1, resolved.length))
208
+ }),
209
+ [view, flying, safeIndex, current, resolved]
210
+ );
211
+ return /* @__PURE__ */ jsx(AtlasContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
212
+ "div",
213
+ {
214
+ ref: rootRef,
215
+ className,
216
+ style: {
217
+ position: "relative",
218
+ width: "100%",
219
+ height: "100%",
220
+ overflow: "hidden",
221
+ ...style
222
+ },
223
+ children: /* @__PURE__ */ jsx(
224
+ "div",
225
+ {
226
+ "data-atlas-camera": "",
227
+ style: {
228
+ position: "absolute",
229
+ inset: 0,
230
+ transform,
231
+ transformOrigin: "0 0",
232
+ // On while flying, off on arrival — see `cameraWillChange`. This one line is
233
+ // the difference between text that is sharp at rest and text that never is.
234
+ willChange: cameraWillChange(flying)
235
+ },
236
+ children: /* @__PURE__ */ jsx(MountedNodes, { mounted, nodes, children })
237
+ }
238
+ )
239
+ }
240
+ ) });
241
+ }
242
+ var MountedContext = createContext(null);
243
+ function MountedNodes({
244
+ mounted,
245
+ nodes,
246
+ children
247
+ }) {
248
+ const declared = useMemo(() => new Set(nodes.map((n) => n.id)), [nodes]);
249
+ const value = useMemo(() => ({ mounted, declared }), [mounted, declared]);
250
+ return /* @__PURE__ */ jsx(MountedContext.Provider, { value, children });
251
+ }
252
+ function useIsMounted(id) {
253
+ const culling = useContext(MountedContext);
254
+ if (!culling || !id) return true;
255
+ if (!culling.declared.has(id)) return true;
256
+ return culling.mounted.has(id);
257
+ }
258
+ function AtlasNode({
259
+ id,
260
+ x,
261
+ y,
262
+ width,
263
+ height,
264
+ children,
265
+ className,
266
+ style
267
+ }) {
268
+ const mounted = useIsMounted(id);
269
+ if (!mounted) return null;
270
+ return /* @__PURE__ */ jsx(
271
+ "div",
272
+ {
273
+ "data-atlas-id": id,
274
+ className,
275
+ style: {
276
+ position: "absolute",
277
+ left: x,
278
+ top: y,
279
+ width,
280
+ height,
281
+ contain: "layout paint",
282
+ ...style
283
+ },
284
+ children
285
+ }
286
+ );
287
+ }
288
+
289
+ export { Atlas, AtlasNode, cameraWillChange, clampIndex, useAtlas, useCamera, useIsMounted, usePrefersReducedMotion };
290
+ //# sourceMappingURL=index.js.map
291
+ //# sourceMappingURL=index.js.map