@entrancekit/react 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 terminalis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @entrancekit/react
2
+
3
+ > Turn an intro animation you already have — a video, a Lottie file, or your own React
4
+ > component — into a **correct** app entrance, without writing the lifecycle yourself.
5
+
6
+ The animation is the easy part. The flow around it is not: playing it on load without
7
+ blocking the app, honoring reduced motion, falling back on slow or failed loads, exiting by
8
+ the model you chose, and tearing down cleanly. **EntranceKit owns that flow. You keep owning
9
+ the animation.**
10
+
11
+ - **Non-blocking** — the overlay never gates your app; it stays interactive underneath.
12
+ - **Reduced-motion aware** — `prefers-reduced-motion` is honored by default (no flash).
13
+ - **Graceful** — skips on error, falls back on timeout, never traps the user.
14
+ - **Self-removing** — deterministic teardown on every exit path, with an optional outro.
15
+ - **Bring your own** — MP4/WebM, Lottie JSON, or any custom React/Motion/GSAP/Canvas/SVG
16
+ component. Client-only, single package, **zero runtime dependencies**, SSR-safe.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install @entrancekit/react
22
+ # Lottie support is optional and lazy-loaded:
23
+ npm install @lottiefiles/dotlottie-react
24
+ ```
25
+
26
+ ## Drop in a video
27
+
28
+ ```tsx
29
+ import { EntranceVideo } from '@entrancekit/react';
30
+
31
+ export default function Root() {
32
+ return (
33
+ <>
34
+ <App /> {/* renders and stays interactive the whole time */}
35
+ <EntranceVideo
36
+ id="launch"
37
+ sources={[
38
+ { src: '/intro.webm', type: 'video/webm' },
39
+ { src: '/intro.mp4', type: 'video/mp4' },
40
+ ]}
41
+ poster="/intro-poster.png"
42
+ fallbackAfter={2500}
43
+ />
44
+ </>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ## Bring your own component (the core)
50
+
51
+ ```tsx
52
+ import { EntranceController } from '@entrancekit/react';
53
+
54
+ <EntranceController id="launch">
55
+ {({ shouldAnimate, complete, skip }) => (
56
+ <MyIntro
57
+ play={shouldAnimate} // start when told
58
+ onComplete={complete} // its OWN done-signal — the preferred path
59
+ onError={skip} // never trap the user
60
+ />
61
+ )}
62
+ </EntranceController>;
63
+ ```
64
+
65
+ ## The lifecycle
66
+
67
+ EntranceKit owns one canonical state machine:
68
+
69
+ ```
70
+ idle → preload → play → resolve → done
71
+ ```
72
+
73
+ Every way an entrance can end — the animation's own done-signal, a user skip, a timeout, an
74
+ error, or a reduced-motion suppression — funnels through a single `resolve` carrying a
75
+ `reason`, so teardown is deterministic on every path. Wire your animation's native end event
76
+ to `complete()`; `fallbackAfter` is only a safety net for a hung or slow load, never a cap on
77
+ a correctly-signalling animation.
78
+
79
+ ## API at a glance
80
+
81
+ | Export | What it is |
82
+ | -------------------- | -------------------------------------------------------------------- |
83
+ | `EntranceController` | Render-prop for a bring-your-own animation. The core. |
84
+ | `EntranceVideo` | MP4/WebM convenience entry point (prefers native `ended`). |
85
+ | `EntranceLottie` | Lottie JSON entry point (optional, lazy-loaded player). |
86
+ | `useEntrance` | The same engine, headless, for a fully custom overlay. |
87
+
88
+ Shared props: `id` (required), `fallbackAfter` (default `4000`), `reducedMotion`
89
+ (`'skip'` \| `'play'`, default `'skip'`), `skipControl` (`true` \| render-fn \| `false`),
90
+ `outro` (`number` \| `{ durationMs, render }`), `container`, and
91
+ `onResolve` / `onError` / `onStateChange`.
92
+
93
+ > There is no `mode`, no persistence, and no show-policy in v0.1.0 — an entrance is a
94
+ > transient overlay that plays and leaves, not a once-per-user gate.
95
+
96
+ ## Demo
97
+
98
+ A live Playground (every source, every scenario, every exit model) is at
99
+ **[terminalis.github.io/entrancekit](https://terminalis.github.io/entrancekit/)** (deployed by
100
+ CI from [`examples/demo`](./examples/demo)). Run it locally:
101
+
102
+ ```sh
103
+ npm install && npm run build
104
+ npm --prefix examples/demo install
105
+ npm --prefix examples/demo run dev
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,556 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var reactDom = require('react-dom');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/useEntrance.ts
9
+
10
+ // src/lifecycle/reducer.ts
11
+ var ACTIVE_PHASES = ["idle", "preload", "play"];
12
+ var GRACEFUL_REASONS = ["completed", "skipped", "timeout"];
13
+ var isActive = (phase) => ACTIVE_PHASES.includes(phase);
14
+ function makeResolve(reason, error = null) {
15
+ return { phase: "resolve", reason, error };
16
+ }
17
+ var initialMachineState = {
18
+ phase: "idle",
19
+ reason: null,
20
+ error: null
21
+ };
22
+ function shouldRunOutro(reason, hasOutro) {
23
+ return hasOutro && GRACEFUL_REASONS.includes(reason);
24
+ }
25
+ function entranceReducer(state, action) {
26
+ switch (action.type) {
27
+ case "BEGIN":
28
+ return state.phase === "idle" ? { ...state, phase: "preload" } : state;
29
+ case "PRELOADED":
30
+ return state.phase === "preload" ? { ...state, phase: "play" } : state;
31
+ case "COMPLETE":
32
+ return state.phase === "preload" || state.phase === "play" ? makeResolve("completed") : state;
33
+ case "SKIP":
34
+ return state.phase === "preload" || state.phase === "play" ? makeResolve("skipped") : state;
35
+ case "TIMEOUT":
36
+ return state.phase === "preload" || state.phase === "play" ? makeResolve("timeout") : state;
37
+ case "FAIL":
38
+ return state.phase === "preload" || state.phase === "play" ? makeResolve("error", {
39
+ kind: action.kind ?? "unknown",
40
+ cause: action.cause
41
+ }) : state;
42
+ case "REDUCED_MOTION":
43
+ return isActive(state.phase) ? makeResolve("reduced-motion") : state;
44
+ case "ENTER_OUTRO":
45
+ return state.phase === "resolve" ? { ...state, phase: "outro" } : state;
46
+ case "ENTER_DONE":
47
+ return state.phase === "resolve" || state.phase === "outro" ? { ...state, phase: "done" } : state;
48
+ default: {
49
+ return state;
50
+ }
51
+ }
52
+ }
53
+
54
+ // src/lifecycle/reducedMotion.ts
55
+ var QUERY = "(prefers-reduced-motion: reduce)";
56
+ function getMediaQueryList() {
57
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
58
+ return null;
59
+ }
60
+ try {
61
+ return window.matchMedia(QUERY);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function prefersReducedMotion() {
67
+ return getMediaQueryList()?.matches ?? false;
68
+ }
69
+ function subscribeReducedMotion(onChange) {
70
+ const mql = getMediaQueryList();
71
+ if (!mql) return () => {
72
+ };
73
+ const handler = (event) => onChange(event.matches);
74
+ if (typeof mql.addEventListener === "function") {
75
+ mql.addEventListener("change", handler);
76
+ return () => mql.removeEventListener("change", handler);
77
+ }
78
+ const legacy = mql;
79
+ legacy.addListener?.(handler);
80
+ return () => legacy.removeListener?.(handler);
81
+ }
82
+
83
+ // src/lifecycle/useEntranceMachine.ts
84
+ function useEntranceMachine(config) {
85
+ const [state, dispatch] = react.useReducer(entranceReducer, initialMachineState);
86
+ const { phase, reason } = state;
87
+ const { reducedMotion, fallbackAfter, preloadTimeout, outroTimeout, hasOutro } = config;
88
+ const actions = react.useMemo(
89
+ () => ({
90
+ preloaded: () => dispatch({ type: "PRELOADED" }),
91
+ complete: () => dispatch({ type: "COMPLETE" }),
92
+ skip: () => dispatch({ type: "SKIP" }),
93
+ fail: (kind, cause) => dispatch({ type: "FAIL", kind, cause }),
94
+ outroComplete: () => dispatch({ type: "ENTER_DONE" })
95
+ }),
96
+ []
97
+ );
98
+ react.useEffect(() => {
99
+ if (reducedMotion === "skip" && prefersReducedMotion()) {
100
+ dispatch({ type: "REDUCED_MOTION" });
101
+ } else {
102
+ dispatch({ type: "BEGIN" });
103
+ }
104
+ if (reducedMotion !== "skip") return;
105
+ return subscribeReducedMotion((reduced) => {
106
+ if (reduced) dispatch({ type: "REDUCED_MOTION" });
107
+ });
108
+ }, [reducedMotion]);
109
+ react.useEffect(() => {
110
+ let id;
111
+ if (phase === "preload") {
112
+ id = setTimeout(() => dispatch({ type: "TIMEOUT" }), preloadTimeout);
113
+ } else if (phase === "play") {
114
+ id = setTimeout(() => dispatch({ type: "TIMEOUT" }), fallbackAfter);
115
+ } else if (phase === "outro") {
116
+ id = setTimeout(() => dispatch({ type: "ENTER_DONE" }), outroTimeout);
117
+ }
118
+ return () => {
119
+ if (id !== void 0) clearTimeout(id);
120
+ };
121
+ }, [phase, preloadTimeout, fallbackAfter, outroTimeout]);
122
+ react.useEffect(() => {
123
+ if (phase !== "resolve" || reason === null) return;
124
+ dispatch(
125
+ shouldRunOutro(reason, hasOutro) ? { type: "ENTER_OUTRO" } : { type: "ENTER_DONE" }
126
+ );
127
+ }, [phase, reason, hasOutro]);
128
+ return { state, actions };
129
+ }
130
+
131
+ // src/lifecycle/defaults.ts
132
+ var DEFAULT_FALLBACK_AFTER = 4e3;
133
+ var DEFAULT_OUTRO_TIMEOUT = 1500;
134
+ var DEFAULT_REDUCED_MOTION = "skip";
135
+ function resolveConfig(input = {}) {
136
+ const fallbackAfter = input.fallbackAfter ?? DEFAULT_FALLBACK_AFTER;
137
+ return {
138
+ fallbackAfter,
139
+ preloadTimeout: input.preloadTimeout ?? fallbackAfter,
140
+ outroTimeout: input.outroTimeout ?? DEFAULT_OUTRO_TIMEOUT,
141
+ reducedMotion: input.reducedMotion ?? DEFAULT_REDUCED_MOTION,
142
+ hasOutro: input.hasOutro ?? false
143
+ };
144
+ }
145
+
146
+ // src/internal/config.ts
147
+ function normalizeOutro(outro) {
148
+ if (outro == null) return { hasOutro: false, durationMs: DEFAULT_OUTRO_TIMEOUT };
149
+ if (typeof outro === "number") return { hasOutro: true, durationMs: outro };
150
+ return { hasOutro: true, durationMs: outro.durationMs, render: outro.render };
151
+ }
152
+ function configFromOptions(input) {
153
+ const outro = normalizeOutro(input.outro);
154
+ return resolveConfig({
155
+ fallbackAfter: input.fallbackAfter,
156
+ reducedMotion: input.reducedMotion,
157
+ hasOutro: outro.hasOutro,
158
+ outroTimeout: outro.durationMs
159
+ });
160
+ }
161
+
162
+ // src/internal/deriveRenderProps.ts
163
+ function deriveRenderProps(state, actions) {
164
+ const { phase, reason } = state;
165
+ return {
166
+ // The transient `outro` phase is surfaced publicly as `'resolve'`.
167
+ state: phase === "outro" ? "resolve" : phase,
168
+ reason,
169
+ shouldAnimate: phase === "play",
170
+ shouldPlayOutro: phase === "outro",
171
+ isOverlayMounted: phase !== "done",
172
+ isPlaying: phase === "play",
173
+ isDone: phase === "done",
174
+ complete: actions.complete,
175
+ skip: actions.skip,
176
+ // Public error path: kind defaults to 'unknown'; the wrappers classify their
177
+ // own errors (load vs playback) via the raw machine action instead.
178
+ fail: (error) => actions.fail(void 0, error),
179
+ outroComplete: actions.outroComplete
180
+ };
181
+ }
182
+ function useEntranceCallbacks(id, state, { onResolve, onError, onStateChange }) {
183
+ const resolveFired = react.useRef(false);
184
+ react.useEffect(() => {
185
+ if (state.reason === null || resolveFired.current) return;
186
+ resolveFired.current = true;
187
+ onResolve?.({ id, reason: state.reason });
188
+ if (state.reason === "error") {
189
+ onError?.({ id, kind: state.error?.kind ?? "unknown", cause: state.error?.cause });
190
+ }
191
+ }, [state.reason, state.error, id, onResolve, onError]);
192
+ const publicState = state.phase === "outro" ? "resolve" : state.phase;
193
+ const lastState = react.useRef(null);
194
+ react.useEffect(() => {
195
+ if (lastState.current === publicState) return;
196
+ lastState.current = publicState;
197
+ onStateChange?.(publicState);
198
+ }, [publicState, onStateChange]);
199
+ }
200
+
201
+ // src/useEntrance.ts
202
+ function useEntrance(options) {
203
+ const { id, fallbackAfter, reducedMotion, outro, onResolve, onError, onStateChange } = options;
204
+ const config = configFromOptions({ fallbackAfter, reducedMotion, outro });
205
+ const { state, actions } = useEntranceMachine(config);
206
+ react.useEffect(() => {
207
+ actions.preloaded();
208
+ }, [actions]);
209
+ useEntranceCallbacks(id, state, { onResolve, onError, onStateChange });
210
+ return deriveRenderProps(state, actions);
211
+ }
212
+ var emptySubscribe = () => () => {
213
+ };
214
+ function useIsClient() {
215
+ return react.useSyncExternalStore(
216
+ emptySubscribe,
217
+ () => true,
218
+ // client snapshot
219
+ () => false
220
+ // server / first-render snapshot
221
+ );
222
+ }
223
+ function useOverlayHost({
224
+ enabled,
225
+ container,
226
+ zIndex
227
+ }) {
228
+ const [host, setHost] = react.useState(null);
229
+ react.useEffect(() => {
230
+ if (!enabled) {
231
+ setHost(null);
232
+ return;
233
+ }
234
+ if (container) {
235
+ setHost(container);
236
+ return;
237
+ }
238
+ const el = document.createElement("div");
239
+ el.setAttribute("data-entrancekit-host", "");
240
+ el.style.position = "fixed";
241
+ el.style.inset = "0";
242
+ el.style.pointerEvents = "none";
243
+ el.style.zIndex = zIndex !== void 0 ? String(zIndex) : "var(--entrancekit-z, 2147483647)";
244
+ document.body.appendChild(el);
245
+ setHost(el);
246
+ return () => {
247
+ el.remove();
248
+ setHost(null);
249
+ };
250
+ }, [enabled, container, zIndex]);
251
+ return host;
252
+ }
253
+ var DEFAULT_SKIP_LABEL = "Skip intro";
254
+ var defaultSkipStyle = {
255
+ position: "absolute",
256
+ top: 16,
257
+ right: 16,
258
+ pointerEvents: "auto",
259
+ padding: "6px 12px",
260
+ font: "inherit",
261
+ fontSize: 14,
262
+ lineHeight: 1.2,
263
+ color: "#fff",
264
+ background: "rgba(0, 0, 0, 0.55)",
265
+ border: "1px solid rgba(255, 255, 255, 0.35)",
266
+ borderRadius: 6,
267
+ cursor: "pointer"
268
+ };
269
+ function EntranceOverlay({
270
+ api,
271
+ children,
272
+ outro,
273
+ skipControl = true,
274
+ skipLabel = DEFAULT_SKIP_LABEL,
275
+ zIndex,
276
+ container,
277
+ className,
278
+ style
279
+ }) {
280
+ const isClient = useIsClient();
281
+ const host = useOverlayHost({
282
+ enabled: isClient && api.isOverlayMounted,
283
+ container,
284
+ zIndex
285
+ });
286
+ const previouslyFocused = react.useRef(null);
287
+ react.useEffect(() => {
288
+ previouslyFocused.current = document.activeElement;
289
+ return () => {
290
+ const prev = previouslyFocused.current;
291
+ const active = document.activeElement;
292
+ if (prev instanceof HTMLElement && active instanceof HTMLElement && active.closest("[data-entrancekit-overlay]")) {
293
+ prev.focus();
294
+ }
295
+ };
296
+ }, []);
297
+ if (!isClient || !host || !api.isOverlayMounted) return null;
298
+ const showVisuals = api.state !== "idle" && api.reason !== "reduced-motion";
299
+ const showSkip = skipControl !== false && (api.state === "preload" || api.state === "play");
300
+ const stageStyle = {
301
+ position: "absolute",
302
+ inset: 0,
303
+ pointerEvents: "none",
304
+ opacity: api.shouldPlayOutro ? 0 : 1,
305
+ transition: api.shouldPlayOutro ? `opacity ${outro.durationMs}ms ease` : void 0,
306
+ ...style
307
+ };
308
+ const visuals = api.shouldPlayOutro && outro.render && api.reason ? outro.render({ reason: api.reason }) : children;
309
+ return reactDom.createPortal(
310
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-entrancekit-overlay": "", className, style: stageStyle, children: [
311
+ showVisuals && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", inset: 0 }, children: visuals }),
312
+ showSkip && (typeof skipControl === "function" ? skipControl(api.skip) : /* @__PURE__ */ jsxRuntime.jsx(
313
+ "button",
314
+ {
315
+ type: "button",
316
+ onClick: api.skip,
317
+ "aria-label": skipLabel,
318
+ "data-entrancekit-skip": "",
319
+ style: defaultSkipStyle,
320
+ children: skipLabel
321
+ }
322
+ ))
323
+ ] }),
324
+ host
325
+ );
326
+ }
327
+ function EntranceController(props) {
328
+ const {
329
+ children,
330
+ skipControl,
331
+ skipLabel,
332
+ zIndex,
333
+ container,
334
+ className,
335
+ style,
336
+ ...lifecycle
337
+ } = props;
338
+ const api = useEntrance(lifecycle);
339
+ return /* @__PURE__ */ jsxRuntime.jsx(
340
+ EntranceOverlay,
341
+ {
342
+ api,
343
+ outro: normalizeOutro(props.outro),
344
+ skipControl,
345
+ skipLabel,
346
+ zIndex,
347
+ container,
348
+ className,
349
+ style,
350
+ children: children(api)
351
+ }
352
+ );
353
+ }
354
+
355
+ // src/internal/useEntranceEngine.ts
356
+ function useEntranceEngine(options) {
357
+ const config = configFromOptions(options);
358
+ const { state, actions } = useEntranceMachine(config);
359
+ useEntranceCallbacks(options.id, state, options);
360
+ return { state, actions, api: deriveRenderProps(state, actions) };
361
+ }
362
+ var videoStyle = {
363
+ position: "absolute",
364
+ inset: 0,
365
+ width: "100%",
366
+ height: "100%",
367
+ objectFit: "cover",
368
+ display: "block"
369
+ };
370
+ function EntranceVideo(props) {
371
+ const {
372
+ id,
373
+ src,
374
+ sources,
375
+ poster,
376
+ muted = true,
377
+ playsInline = true,
378
+ preload = "auto",
379
+ fallbackAfter,
380
+ reducedMotion,
381
+ outro,
382
+ skipControl,
383
+ skipLabel,
384
+ zIndex,
385
+ container,
386
+ className,
387
+ style,
388
+ onResolve,
389
+ onError,
390
+ onStateChange
391
+ } = props;
392
+ const { state, actions, api } = useEntranceEngine({
393
+ id,
394
+ fallbackAfter,
395
+ reducedMotion,
396
+ outro,
397
+ onResolve,
398
+ onError,
399
+ onStateChange
400
+ });
401
+ const videoRef = react.useRef(null);
402
+ const endedFired = react.useRef(false);
403
+ react.useEffect(() => {
404
+ if (state.phase !== "play") return;
405
+ const video = videoRef.current;
406
+ if (!video) return;
407
+ try {
408
+ video.muted = muted;
409
+ const result = video.play();
410
+ if (result && typeof result.then === "function") {
411
+ result.catch((err) => actions.fail("playback", err));
412
+ }
413
+ } catch (err) {
414
+ actions.fail("playback", err);
415
+ }
416
+ }, [state.phase, actions, muted]);
417
+ return /* @__PURE__ */ jsxRuntime.jsx(
418
+ EntranceOverlay,
419
+ {
420
+ api,
421
+ outro: normalizeOutro(outro),
422
+ skipControl,
423
+ skipLabel,
424
+ zIndex,
425
+ container,
426
+ className,
427
+ style,
428
+ children: /* @__PURE__ */ jsxRuntime.jsx(
429
+ "video",
430
+ {
431
+ ref: videoRef,
432
+ ...src ? { src } : {},
433
+ poster,
434
+ muted,
435
+ playsInline,
436
+ preload,
437
+ onCanPlayThrough: () => actions.preloaded(),
438
+ onLoadedData: () => actions.preloaded(),
439
+ onEnded: () => {
440
+ if (endedFired.current) return;
441
+ endedFired.current = true;
442
+ actions.complete();
443
+ },
444
+ onError: () => actions.fail("load"),
445
+ style: videoStyle,
446
+ children: sources?.map((source) => /* @__PURE__ */ jsxRuntime.jsx("source", { src: source.src, type: source.type }, source.src))
447
+ }
448
+ )
449
+ }
450
+ );
451
+ }
452
+ var PEER = "@lottiefiles/dotlottie-react";
453
+ var playerStyle = {
454
+ position: "absolute",
455
+ inset: 0,
456
+ width: "100%",
457
+ height: "100%",
458
+ display: "block"
459
+ };
460
+ function EntranceLottie(props) {
461
+ const {
462
+ id,
463
+ src,
464
+ animationData,
465
+ onComplete,
466
+ fallbackAfter,
467
+ reducedMotion,
468
+ outro,
469
+ skipControl,
470
+ skipLabel,
471
+ zIndex,
472
+ container,
473
+ className,
474
+ style,
475
+ onResolve,
476
+ onError,
477
+ onStateChange
478
+ } = props;
479
+ const { state, actions, api } = useEntranceEngine({
480
+ id,
481
+ fallbackAfter,
482
+ reducedMotion,
483
+ outro,
484
+ onResolve,
485
+ onError,
486
+ onStateChange
487
+ });
488
+ const [Player, setPlayer] = react.useState(null);
489
+ const shouldLoad = state.phase === "preload" || state.phase === "play";
490
+ react.useEffect(() => {
491
+ if (!shouldLoad) return;
492
+ let alive = true;
493
+ const reportMissingPeer = (err) => {
494
+ if (typeof console !== "undefined") {
495
+ console.error(
496
+ `[EntranceKit] EntranceLottie requires the optional peer "${PEER}". Install it with: npm i ${PEER}`
497
+ );
498
+ }
499
+ actions.fail("missing-peer", err);
500
+ };
501
+ import('@lottiefiles/dotlottie-react').then((mod) => {
502
+ if (!alive) return;
503
+ const Comp = mod.DotLottieReact;
504
+ if (Comp) setPlayer(() => Comp);
505
+ else reportMissingPeer(new Error(`${PEER} did not export DotLottieReact`));
506
+ }).catch((err) => {
507
+ if (alive) reportMissingPeer(err);
508
+ });
509
+ return () => {
510
+ alive = false;
511
+ };
512
+ }, [shouldLoad, actions]);
513
+ const handleRef = react.useCallback(
514
+ (instance) => {
515
+ if (!instance) return;
516
+ instance.addEventListener("load", () => actions.preloaded());
517
+ instance.addEventListener("complete", () => {
518
+ actions.complete();
519
+ onComplete?.();
520
+ });
521
+ instance.addEventListener("loadError", () => actions.fail("load"));
522
+ },
523
+ [actions, onComplete]
524
+ );
525
+ return /* @__PURE__ */ jsxRuntime.jsx(
526
+ EntranceOverlay,
527
+ {
528
+ api,
529
+ outro: normalizeOutro(outro),
530
+ skipControl,
531
+ skipLabel,
532
+ zIndex,
533
+ container,
534
+ className,
535
+ style,
536
+ children: Player ? /* @__PURE__ */ jsxRuntime.jsx(
537
+ Player,
538
+ {
539
+ ...src ? { src } : {},
540
+ data: animationData,
541
+ loop: false,
542
+ autoplay: true,
543
+ dotLottieRefCallback: handleRef,
544
+ style: playerStyle
545
+ }
546
+ ) : null
547
+ }
548
+ );
549
+ }
550
+
551
+ exports.EntranceController = EntranceController;
552
+ exports.EntranceLottie = EntranceLottie;
553
+ exports.EntranceVideo = EntranceVideo;
554
+ exports.useEntrance = useEntrance;
555
+ //# sourceMappingURL=index.cjs.map
556
+ //# sourceMappingURL=index.cjs.map