@srsergio/taptapp-ar 1.0.53 → 1.0.55

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/README.md CHANGED
@@ -131,12 +131,57 @@ ar.stop();
131
131
  | `overlay` | ✅ | DOM element to position on the target |
132
132
  | `onFound` | ❌ | Callback when target is detected |
133
133
  | `onLost` | ❌ | Callback when target is lost |
134
- | `onUpdate` | ❌ | Called each frame with `{ targetIndex, worldMatrix }` |
135
- | `cameraConfig` | ❌ | Camera constraints (default: `{ facingMode: 'environment', width: 1280, height: 720 }`) |
134
+ | `onUpdate` | ❌ | Called cada frame con `{ targetIndex, worldMatrix }` |
135
+ | `cameraConfig` | ❌ | Config de cámara (por defecto: `{ facingMode: 'environment', width: 1280, height: 720 }`) |
136
136
 
137
137
  ---
138
138
 
139
- ### 2. Raw Controller (Advanced & Custom Engines)
139
+ ### 2. React Integration (Vite & SSR Safe) ⚛️
140
+
141
+ The fastest and most modern way to build AR apps with React. It supports **Code Splitting** and is **100% SSR-Safe** (Next.js, Astro, Remix).
142
+
143
+ #### 🚀 Quick Start: `<TaptappAR />`
144
+ Drop the component into your app. It handles camera permissions, scanning animations, and video/image overlays automatically.
145
+
146
+ ```tsx
147
+ import { TaptappAR, mapDataToPropsConfig } from '@srsergio/taptapp-ar';
148
+
149
+ const MyARComponent = ({ data }) => {
150
+ // mapDataToPropsConfig helps you format your data for the component
151
+ const config = mapDataToPropsConfig(data);
152
+
153
+ return (
154
+ <div style={{ width: '100vw', height: '100vh' }}>
155
+ <TaptappAR config={config} />
156
+ </div>
157
+ );
158
+ };
159
+ ```
160
+
161
+ #### 🛠️ Custom UI: `useAR()` Hook
162
+ If you want to build your own UI while keeping the powerful tracking logic:
163
+
164
+ ```tsx
165
+ import { useAR } from '@srsergio/taptapp-ar';
166
+
167
+ const CustomAR = ({ config }) => {
168
+ const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
169
+
170
+ return (
171
+ <div ref={containerRef} style={{ position: 'relative' }} onClick={toggleVideo}>
172
+ {/* Custom Scanning UI */}
173
+ {status === 'scanning' && <div className="my-loader">Scanning...</div>}
174
+
175
+ {/* Video Overlay */}
176
+ <video ref={overlayRef} src={config.videoSrc} loop muted playsInline />
177
+ </div>
178
+ );
179
+ };
180
+ ```
181
+
182
+ ---
183
+
184
+ ### 3. Raw Controller (Advanced & Custom Engines)
140
185
  The `Controller` is the core engine of TapTapp AR. You can use it to build your own AR components or integrate tracking into custom 3D engines.
141
186
 
142
187
  #### ⚙️ Controller Configuration
@@ -216,8 +261,15 @@ TapTapp AR uses a proprietary **Moonshot Vision Codec** that is significantly mo
216
261
 
217
262
  ---
218
263
 
219
- ## 📄 License & Credits
264
+ ## 📄 License & Recognition
265
+
266
+ **Taptapp AR** is created and maintained by **Sergio Lazaro**.
267
+
268
+ This project is licensed under the **GPL-3.0 License**.
269
+ This ensures that the project remains open and free, and that authorship is properly recognized. No "closed-source" usage is allowed without a commercial agreement.
270
+
271
+ Commercial licenses are available for proprietary applications. Please contact the author for details.
220
272
 
221
- MIT © [srsergiolazaro](https://github.com/srsergiolazaro)
273
+ ### Acknowledgements
274
+ This project evolved from the incredible work of [MindAR](https://github.com/hiukim/mind-ar-js). While the codebase has been extensively rewritten and optimized for performance, we gratefully acknowledge the foundation laid by the original authors.
222
275
 
223
- Based on the core research of MindAR, but completely re-written for high-performance binary processing and JS-only execution.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from "./react/types.js";
2
+ export * from "./react/use-ar.js";
3
+ export * from "./react/TaptappAR.js";
2
4
  export * from "./compiler/offline-compiler.js";
3
5
  export { Controller } from "./compiler/controller.js";
4
6
  export { SimpleAR } from "./compiler/simple-ar.js";
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from "./react/types.js";
2
+ export * from "./react/use-ar.js";
3
+ export * from "./react/TaptappAR.js";
2
4
  export * from "./compiler/offline-compiler.js";
3
5
  export { Controller } from "./compiler/controller.js";
4
6
  export { SimpleAR } from "./compiler/simple-ar.js";
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import type { PropsConfig } from "./types.js";
3
+ export interface TaptappARProps {
4
+ config: PropsConfig;
5
+ className?: string;
6
+ showScanningOverlay?: boolean;
7
+ showErrorOverlay?: boolean;
8
+ }
9
+ export declare const TaptappAR: React.FC<TaptappARProps>;
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { useAR } from "./use-ar.js";
4
+ export const TaptappAR = ({ config, className = "", showScanningOverlay = true, showErrorOverlay = true }) => {
5
+ const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
6
+ // Simple heuristic to determine if it's a video or image
7
+ // based on the presence of videoSrc and common extensions
8
+ const isVideo = useMemo(() => {
9
+ if (!config.videoSrc)
10
+ return false;
11
+ const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'];
12
+ const url = config.videoSrc.toLowerCase().split('?')[0];
13
+ return videoExtensions.some(ext => url.endsWith(ext)) || config.videoSrc.includes('video');
14
+ }, [config.videoSrc]);
15
+ return (_jsxs("div", { className: `taptapp-ar-wrapper ${className} ${status}`, style: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }, children: [showScanningOverlay && status === "scanning" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-scanning", children: _jsxs("div", { className: "scanning-content", children: [_jsxs("div", { className: "scanning-frame", children: [_jsx("img", { className: "target-preview", src: config.targetImageSrc, alt: "Target", crossOrigin: "anonymous" }), _jsx("div", { className: "scanning-line" })] }), _jsx("p", { className: "scanning-text", children: "Apunta a la imagen para comenzar" })] }) })), showErrorOverlay && status === "error" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-error", children: _jsxs("div", { className: "error-content", children: [_jsx("span", { className: "error-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "error-title", children: "No se pudo iniciar AR" }), _jsx("p", { className: "error-text", children: "Verifica los permisos de c\u00E1mara" }), _jsx("button", { className: "retry-btn", onClick: () => window.location.reload(), children: "Reintentar" })] }) })), _jsx("div", { ref: containerRef, className: "taptapp-ar-container", onClick: toggleVideo, style: { width: '100%', height: '100%' }, children: isVideo ? (_jsx("video", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc, preload: "auto", loop: true, playsInline: true, muted: true, crossOrigin: "anonymous" })) : (_jsx("img", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc || config.targetImageSrc, crossOrigin: "anonymous", alt: "AR Overlay" })) }), _jsx("style", { children: `
16
+ .taptapp-ar-wrapper {
17
+ background: #000;
18
+ color: white;
19
+ font-family: system-ui, -apple-system, sans-serif;
20
+ }
21
+ .taptapp-ar-overlay {
22
+ position: absolute;
23
+ top: 0;
24
+ left: 0;
25
+ right: 0;
26
+ bottom: 0;
27
+ z-index: 20;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background: rgba(0,0,0,0.7);
32
+ backdrop-filter: blur(4px);
33
+ transition: opacity 0.3s ease;
34
+ }
35
+ .scanning-content, .error-content {
36
+ text-align: center;
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: center;
40
+ padding: 20px;
41
+ }
42
+ .scanning-frame {
43
+ position: relative;
44
+ width: 200px;
45
+ height: 200px;
46
+ border: 2px solid rgba(255,255,255,0.3);
47
+ border-radius: 20px;
48
+ overflow: hidden;
49
+ margin-bottom: 20px;
50
+ }
51
+ .target-preview {
52
+ width: 100%;
53
+ height: 100%;
54
+ object-fit: cover;
55
+ opacity: 0.6;
56
+ }
57
+ .scanning-line {
58
+ position: absolute;
59
+ top: 0;
60
+ left: 0;
61
+ width: 100%;
62
+ height: 2px;
63
+ background: #00e5ff;
64
+ box-shadow: 0 0 15px #00e5ff;
65
+ animation: scan 2s linear infinite;
66
+ }
67
+ @keyframes scan {
68
+ 0% { top: 0; }
69
+ 50% { top: 100%; }
70
+ 100% { top: 0; }
71
+ }
72
+ .scanning-text {
73
+ font-size: 1.1rem;
74
+ font-weight: 500;
75
+ letter-spacing: 0.5px;
76
+ }
77
+ .error-icon { font-size: 3rem; margin-bottom: 10px; }
78
+ .error-title { font-size: 1.2rem; font-weight: bold; margin: 0; }
79
+ .error-text { opacity: 0.8; margin: 5px 0 20px; }
80
+ .retry-btn {
81
+ padding: 10px 25px;
82
+ border-radius: 30px;
83
+ border: none;
84
+ background: #fff;
85
+ color: #000;
86
+ font-weight: 600;
87
+ cursor: pointer;
88
+ transition: transform 0.2s;
89
+ }
90
+ .retry-btn:active { transform: scale(0.95); }
91
+ .taptapp-ar-overlay-element {
92
+ display: block;
93
+ width: 100%;
94
+ height: auto;
95
+ }
96
+ ` })] }));
97
+ };
@@ -0,0 +1,10 @@
1
+ import type { PropsConfig } from "./types.js";
2
+ export type ARStatus = "scanning" | "tracking" | "error";
3
+ export interface UseARReturn {
4
+ containerRef: React.RefObject<HTMLDivElement>;
5
+ overlayRef: React.RefObject<HTMLVideoElement | HTMLImageElement>;
6
+ status: ARStatus;
7
+ isPlaying: boolean;
8
+ toggleVideo: () => Promise<void>;
9
+ }
10
+ export declare const useAR: (config: PropsConfig) => UseARReturn;
@@ -0,0 +1,94 @@
1
+ import { useEffect, useRef, useState, useCallback } from "react";
2
+ export const useAR = (config) => {
3
+ const containerRef = useRef(null);
4
+ const overlayRef = useRef(null);
5
+ const [status, setStatus] = useState("scanning");
6
+ const [isPlaying, setIsPlaying] = useState(false);
7
+ const arInstanceRef = useRef(null);
8
+ const toggleVideo = useCallback(async () => {
9
+ const overlay = overlayRef.current;
10
+ if (!(overlay instanceof HTMLVideoElement))
11
+ return;
12
+ try {
13
+ if (overlay.paused) {
14
+ await overlay.play();
15
+ setIsPlaying(true);
16
+ }
17
+ else {
18
+ overlay.pause();
19
+ setIsPlaying(false);
20
+ }
21
+ }
22
+ catch (err) {
23
+ console.error("Error toggling video:", err);
24
+ }
25
+ }, []);
26
+ useEffect(() => {
27
+ if (typeof window === "undefined" || !containerRef.current || !overlayRef.current)
28
+ return;
29
+ let isMounted = true;
30
+ const initAR = async () => {
31
+ try {
32
+ // Safe hybrid import for SSR + Speed
33
+ const { SimpleAR } = await import("../compiler/simple-ar.js");
34
+ if (!isMounted)
35
+ return;
36
+ const instance = new SimpleAR({
37
+ container: containerRef.current,
38
+ targetSrc: config.targetTaarSrc,
39
+ overlay: overlayRef.current,
40
+ scale: config.scale,
41
+ onFound: async ({ targetIndex }) => {
42
+ console.log(`🎯 Target ${targetIndex} detected!`);
43
+ if (!isMounted)
44
+ return;
45
+ setStatus("tracking");
46
+ const overlay = overlayRef.current;
47
+ if (overlay instanceof HTMLVideoElement) {
48
+ try {
49
+ await overlay.play();
50
+ setIsPlaying(true);
51
+ }
52
+ catch (err) {
53
+ console.warn("Auto-play blocked:", err);
54
+ }
55
+ }
56
+ },
57
+ onLost: ({ targetIndex }) => {
58
+ console.log(`👋 Target ${targetIndex} lost`);
59
+ if (!isMounted)
60
+ return;
61
+ setStatus("scanning");
62
+ const overlay = overlayRef.current;
63
+ if (overlay instanceof HTMLVideoElement) {
64
+ overlay.pause();
65
+ setIsPlaying(false);
66
+ }
67
+ }
68
+ });
69
+ arInstanceRef.current = instance;
70
+ await instance.start();
71
+ if (isMounted)
72
+ setStatus("scanning");
73
+ }
74
+ catch (err) {
75
+ console.error("Failed to initialize AR:", err);
76
+ if (isMounted)
77
+ setStatus("error");
78
+ }
79
+ };
80
+ initAR();
81
+ return () => {
82
+ isMounted = false;
83
+ arInstanceRef.current?.stop();
84
+ arInstanceRef.current = null;
85
+ };
86
+ }, [config.targetTaarSrc, config.scale]);
87
+ return {
88
+ containerRef,
89
+ overlayRef,
90
+ status,
91
+ isPlaying,
92
+ toggleVideo
93
+ };
94
+ };
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
+ "author": "Sergio Lazaro <srsergiolazaro@gmail.com>",
5
+ "license": "GPL-3.0",
4
6
  "description": "Ultra-fast, lightweight Augmented Reality Image Tracking SDK for the web. Features an optimized offline compiler, React components, and compatibility with Three.js/A-Frame. No heavy ML frameworks required.",
5
7
  "keywords": [
6
8
  "web-ar",
package/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from "./react/types.js";
2
+ export * from "./react/use-ar.js";
3
+ export * from "./react/TaptappAR.js";
2
4
  export * from "./compiler/offline-compiler.js";
3
5
  export { Controller } from "./compiler/controller.js";
4
6
  export { SimpleAR } from "./compiler/simple-ar.js";
7
+
@@ -0,0 +1,176 @@
1
+ import React, { useMemo } from "react";
2
+ import { useAR } from "./use-ar.js";
3
+ import type { PropsConfig } from "./types.js";
4
+
5
+ export interface TaptappARProps {
6
+ config: PropsConfig;
7
+ className?: string;
8
+ showScanningOverlay?: boolean;
9
+ showErrorOverlay?: boolean;
10
+ }
11
+
12
+ export const TaptappAR: React.FC<TaptappARProps> = ({
13
+ config,
14
+ className = "",
15
+ showScanningOverlay = true,
16
+ showErrorOverlay = true
17
+ }) => {
18
+ const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
19
+
20
+ // Simple heuristic to determine if it's a video or image
21
+ // based on the presence of videoSrc and common extensions
22
+ const isVideo = useMemo(() => {
23
+ if (!config.videoSrc) return false;
24
+ const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov'];
25
+ const url = config.videoSrc.toLowerCase().split('?')[0];
26
+ return videoExtensions.some(ext => url.endsWith(ext)) || config.videoSrc.includes('video');
27
+ }, [config.videoSrc]);
28
+
29
+ return (
30
+ <div className={`taptapp-ar-wrapper ${className} ${status}`} style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
31
+ {/* Scanning Overlay */}
32
+ {showScanningOverlay && status === "scanning" && (
33
+ <div className="taptapp-ar-overlay taptapp-ar-scanning">
34
+ <div className="scanning-content">
35
+ <div className="scanning-frame">
36
+ <img
37
+ className="target-preview"
38
+ src={config.targetImageSrc}
39
+ alt="Target"
40
+ crossOrigin="anonymous"
41
+ />
42
+ <div className="scanning-line"></div>
43
+ </div>
44
+ <p className="scanning-text">Apunta a la imagen para comenzar</p>
45
+ </div>
46
+ </div>
47
+ )}
48
+
49
+ {/* Error Overlay */}
50
+ {showErrorOverlay && status === "error" && (
51
+ <div className="taptapp-ar-overlay taptapp-ar-error">
52
+ <div className="error-content">
53
+ <span className="error-icon">⚠️</span>
54
+ <p className="error-title">No se pudo iniciar AR</p>
55
+ <p className="error-text">Verifica los permisos de cámara</p>
56
+ <button className="retry-btn" onClick={() => window.location.reload()}>
57
+ Reintentar
58
+ </button>
59
+ </div>
60
+ </div>
61
+ )}
62
+
63
+ {/* AR Container */}
64
+ <div
65
+ ref={containerRef}
66
+ className="taptapp-ar-container"
67
+ onClick={toggleVideo}
68
+ style={{ width: '100%', height: '100%' }}
69
+ >
70
+ {isVideo ? (
71
+ <video
72
+ ref={overlayRef as React.RefObject<HTMLVideoElement>}
73
+ className="taptapp-ar-overlay-element"
74
+ src={config.videoSrc}
75
+ preload="auto"
76
+ loop
77
+ playsInline
78
+ muted
79
+ crossOrigin="anonymous"
80
+ />
81
+ ) : (
82
+ <img
83
+ ref={overlayRef as React.RefObject<HTMLImageElement>}
84
+ className="taptapp-ar-overlay-element"
85
+ src={config.videoSrc || config.targetImageSrc}
86
+ crossOrigin="anonymous"
87
+ alt="AR Overlay"
88
+ />
89
+ )}
90
+ </div>
91
+
92
+ <style>{`
93
+ .taptapp-ar-wrapper {
94
+ background: #000;
95
+ color: white;
96
+ font-family: system-ui, -apple-system, sans-serif;
97
+ }
98
+ .taptapp-ar-overlay {
99
+ position: absolute;
100
+ top: 0;
101
+ left: 0;
102
+ right: 0;
103
+ bottom: 0;
104
+ z-index: 20;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ background: rgba(0,0,0,0.7);
109
+ backdrop-filter: blur(4px);
110
+ transition: opacity 0.3s ease;
111
+ }
112
+ .scanning-content, .error-content {
113
+ text-align: center;
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: center;
117
+ padding: 20px;
118
+ }
119
+ .scanning-frame {
120
+ position: relative;
121
+ width: 200px;
122
+ height: 200px;
123
+ border: 2px solid rgba(255,255,255,0.3);
124
+ border-radius: 20px;
125
+ overflow: hidden;
126
+ margin-bottom: 20px;
127
+ }
128
+ .target-preview {
129
+ width: 100%;
130
+ height: 100%;
131
+ object-fit: cover;
132
+ opacity: 0.6;
133
+ }
134
+ .scanning-line {
135
+ position: absolute;
136
+ top: 0;
137
+ left: 0;
138
+ width: 100%;
139
+ height: 2px;
140
+ background: #00e5ff;
141
+ box-shadow: 0 0 15px #00e5ff;
142
+ animation: scan 2s linear infinite;
143
+ }
144
+ @keyframes scan {
145
+ 0% { top: 0; }
146
+ 50% { top: 100%; }
147
+ 100% { top: 0; }
148
+ }
149
+ .scanning-text {
150
+ font-size: 1.1rem;
151
+ font-weight: 500;
152
+ letter-spacing: 0.5px;
153
+ }
154
+ .error-icon { font-size: 3rem; margin-bottom: 10px; }
155
+ .error-title { font-size: 1.2rem; font-weight: bold; margin: 0; }
156
+ .error-text { opacity: 0.8; margin: 5px 0 20px; }
157
+ .retry-btn {
158
+ padding: 10px 25px;
159
+ border-radius: 30px;
160
+ border: none;
161
+ background: #fff;
162
+ color: #000;
163
+ font-weight: 600;
164
+ cursor: pointer;
165
+ transition: transform 0.2s;
166
+ }
167
+ .retry-btn:active { transform: scale(0.95); }
168
+ .taptapp-ar-overlay-element {
169
+ display: block;
170
+ width: 100%;
171
+ height: auto;
172
+ }
173
+ `}</style>
174
+ </div>
175
+ );
176
+ };
@@ -0,0 +1,109 @@
1
+ import { useEffect, useRef, useState, useCallback } from "react";
2
+ import type { PropsConfig } from "./types.js";
3
+ import type { SimpleAR as SimpleARType } from "../compiler/simple-ar.js";
4
+
5
+ export type ARStatus = "scanning" | "tracking" | "error";
6
+
7
+ export interface UseARReturn {
8
+ containerRef: React.RefObject<HTMLDivElement>;
9
+ overlayRef: React.RefObject<HTMLVideoElement | HTMLImageElement>;
10
+ status: ARStatus;
11
+ isPlaying: boolean;
12
+ toggleVideo: () => Promise<void>;
13
+ }
14
+
15
+ export const useAR = (config: PropsConfig): UseARReturn => {
16
+ const containerRef = useRef<HTMLDivElement>(null);
17
+ const overlayRef = useRef<HTMLVideoElement | HTMLImageElement>(null);
18
+ const [status, setStatus] = useState<ARStatus>("scanning");
19
+ const [isPlaying, setIsPlaying] = useState(false);
20
+ const arInstanceRef = useRef<SimpleARType | null>(null);
21
+
22
+ const toggleVideo = useCallback(async () => {
23
+ const overlay = overlayRef.current;
24
+ if (!(overlay instanceof HTMLVideoElement)) return;
25
+
26
+ try {
27
+ if (overlay.paused) {
28
+ await overlay.play();
29
+ setIsPlaying(true);
30
+ } else {
31
+ overlay.pause();
32
+ setIsPlaying(false);
33
+ }
34
+ } catch (err) {
35
+ console.error("Error toggling video:", err);
36
+ }
37
+ }, []);
38
+
39
+ useEffect(() => {
40
+ if (typeof window === "undefined" || !containerRef.current || !overlayRef.current) return;
41
+
42
+ let isMounted = true;
43
+
44
+ const initAR = async () => {
45
+ try {
46
+ // Safe hybrid import for SSR + Speed
47
+ const { SimpleAR } = await import("../compiler/simple-ar.js");
48
+ if (!isMounted) return;
49
+
50
+ const instance = new SimpleAR({
51
+ container: containerRef.current!,
52
+ targetSrc: config.targetTaarSrc,
53
+ overlay: overlayRef.current!,
54
+ scale: config.scale,
55
+ onFound: async ({ targetIndex }) => {
56
+ console.log(`🎯 Target ${targetIndex} detected!`);
57
+ if (!isMounted) return;
58
+ setStatus("tracking");
59
+
60
+ const overlay = overlayRef.current;
61
+ if (overlay instanceof HTMLVideoElement) {
62
+ try {
63
+ await overlay.play();
64
+ setIsPlaying(true);
65
+ } catch (err) {
66
+ console.warn("Auto-play blocked:", err);
67
+ }
68
+ }
69
+ },
70
+ onLost: ({ targetIndex }) => {
71
+ console.log(`👋 Target ${targetIndex} lost`);
72
+ if (!isMounted) return;
73
+ setStatus("scanning");
74
+
75
+ const overlay = overlayRef.current;
76
+ if (overlay instanceof HTMLVideoElement) {
77
+ overlay.pause();
78
+ setIsPlaying(false);
79
+ }
80
+ }
81
+ });
82
+
83
+ arInstanceRef.current = instance;
84
+ await instance.start();
85
+
86
+ if (isMounted) setStatus("scanning");
87
+ } catch (err) {
88
+ console.error("Failed to initialize AR:", err);
89
+ if (isMounted) setStatus("error");
90
+ }
91
+ };
92
+
93
+ initAR();
94
+
95
+ return () => {
96
+ isMounted = false;
97
+ arInstanceRef.current?.stop();
98
+ arInstanceRef.current = null;
99
+ };
100
+ }, [config.targetTaarSrc, config.scale]);
101
+
102
+ return {
103
+ containerRef,
104
+ overlayRef,
105
+ status,
106
+ isPlaying,
107
+ toggleVideo
108
+ };
109
+ };