import { useCallback, useEffect, useRef, useState } from 'react'; import ScannerToast from './ScannerToast.js'; import ScannerCountPill from './ScannerCountPill.js'; import DeckModeIndicator from './DeckModeIndicator.js'; import ScannerDisambiguation from './ScannerDisambiguation.js'; import { useScannerSound } from '../../lib/use-scanner-sound.js'; import { useScannerFlash } from '../../lib/use-scanner-flash.js'; const TOAST_DURATION_MS = 2500; function overlayBorderColor(status) { if (status === 'scanned') return 'var(--color-info, #3B82F6)'; return 'var(--color-success, #10B981)'; } function overlayGlowColor(status) { if (status === 'scanned') return 'rgba(59, 130, 246, 0.31)'; return 'rgba(16, 185, 129, 0.31)'; } export default function ScannerCamera({ queue, camera, identification, deckMode, onReview, onStopSession, sessionDestination, }) { const [toast, setToast] = useState({ message: '', visible: false, type: 'success' }); const toastTimerRef = useRef(null); const prevCountRef = useRef(queue.scannedCards?.length ?? 0); const { videoRef, canvasRef, detectionCanvasRef, isStreaming, isDetecting, trackedCards, videoMetrics, startCamera, stopCamera, streamRef, } = camera; const sound = useScannerSound(); // Requires useCameraScanner to expose streamRef — gracefully degrades to flash-unsupported if absent const flash = useScannerFlash(streamRef); // Auto-start camera on mount useEffect(() => { startCamera(); // eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot mount }, []); // React to newly scanned cards const currentCount = queue.scannedCards?.length ?? 0; useEffect(() => { if (currentCount > prevCountRef.current) { const newest = queue.scannedCards[0]; const cardName = newest?.name || newest?.card?.name || 'Card scanned'; navigator.vibrate?.(50); sound.playSuccess(); clearTimeout(toastTimerRef.current); setToast({ message: cardName, visible: true, type: 'success' }); toastTimerRef.current = setTimeout(() => { setToast((t) => ({ ...t, visible: false })); }, TOAST_DURATION_MS); } prevCountRef.current = currentCount; }, [currentCount]); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally only reacts to count useEffect(() => { return () => clearTimeout(toastTimerRef.current); }, []); const handleStop = useCallback(() => { stopCamera(); onStopSession?.(); }, [stopCamera, onStopSession]); const foundCards = trackedCards.filter( (c) => c.status === 'confirmed' || c.status === 'scanned' ); const hasMetrics = videoMetrics.width > 0 && videoMetrics.height > 0; return (