El navegador moderno ha dejado de ser un simple renderizador aislado de hipertexto. En la actualidad, la plataforma web proporciona acceso directo a las capacidades de hardware de los dispositivos a través de estándares W3C/WHATWG conocidos como Browser Hardware APIs.
En las interfaces actuales, los usuarios esperan un nivel de funcionalidad equiparable al de las aplicaciones nativas de escritorio y móviles:
No obstante, integrar estas interfaces de bajo nivel en el modelo de componentes de React entraña riesgos arquitectónicos críticos:
navigator o window en React Server Components de Next.js aborta inmediatamente el proceso de Node.js.https://) o sin un gesto de usuario directo (Transient User Activation) es bloqueado por el navegador.Los recursos físicos del sistema operativo (cámara, micrófono, bloqueo de suspensión de pantalla) no están controlados por el recolector de basura de JavaScript V8. Todo flujo capturado MediaStream debe tener un ciclo de cierre determinista mediante track.stop(), y las llamadas a APIs protegidas deben estar blindadas por comprobaciones isSecureContext y vinculadas a eventos directos de activación de usuario (Transient User Activation).
La interacción entre una aplicación React y los subsistemas de hardware del sistema operativo se rige por un estricto modelo de seguridad de tres niveles:
┌─────────────────────────────────────────────────────────────┐
│ REACT 19 COMPONENT (UI Layer) │
│ useClipboard() │ useMediaDevices() │ useWakeLock() │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ BROWSER SECURITY & PERMISSION GATEWAY │
│ 1. Secure Context Check: window.isSecureContext (HTTPS) │
│ 2. Transient User Activation: event.isTrusted (User Click) │
│ 3. Permissions API: Query & Reactive Status ('granted') │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OPERATING SYSTEM HARDWARE SUBSYSTEMS │
│ • MediaDevices (Webcam, Mic, Screen Share) │
│ • System Clipboard (Text, HTML, PNG Blobs) │
│ • Screen Wake Lock (Power Management Subsystem) │
│ • Web Share (Native OS Share Sheet) │
└─────────────────────────────────────────────────────────────┘mediaDevices, clipboard, wakeLock, credentials) solo se exponen en navigator a través del protocolo HTTPS o en localhost. Bajo HTTP inseguro, estas interfaces son undefined.navigator.share), la escritura en el portapapeles (clipboard.writeText) o la captura de pantalla (getDisplayMedia) requieren una interacción física reciente y directa del usuario (clic o pulsación de tecla). Intentar ejecutarlas desde temporizadores (setTimeout) o promesas en segundo plano falla con NotAllowedError.MediaStream es solo un puntero en memoria JavaScript. El dispositivo físico de la cámara permanece bloqueado por el controlador del kernel del sistema operativo. Al desmontar el componente React, es obligatorio invocar explícitamente track.stop() en cada pista del array devuelto por stream.getTracks().Comprobar typeof window !== 'undefined' y window.isSecureContext para evitar caídas durante la ejecución en servidor.
Consultar navigator.permissions.query() para evaluar el estado actual ('granted', 'prompt', 'denied') sin diálogos molestos.
Ejecutar navigator.mediaDevices.getUserMedia() estrictamente dentro de un manejador de evento de confianza (onClick).
Invocar track.stop() en pistas de audio/vídeo y sentinel.release() para el bloqueo de pantalla en la función cleanup de useEffect.
Un usuario abre una ventana modal de verificación de identidad con previsualización de cámara web (<video srcObject={stream} />) y luego pulsa el botón de cerrar. React desmonta con éxito el componente modal del DOM, pero en el portátil del usuario el indicador LED verde de la cámara continúa encendido. ¿Cuál es la causa de esta fuga de hardware?
Antes de ingresar a una sala de conferencias, la aplicación efectúa una comprobación previa (preflight): enumera los dispositivos mediante enumerateDevices(), analiza el nivel de volumen del micrófono con AudioContext y permite alternar entre dispositivos de entrada sin recargar la página.
Un rastreador de entrenamiento muestra un temporizador por intervalos y un portal de cocina detalla los pasos de una receta. Para evitar que el usuario deba tocar la pantalla con las manos ocupadas, la aplicación mantiene la pantalla encendida mediante Screen Wake Lock API, recuperando el bloqueo automáticamente al volver a la pestaña.
Copia instantánea de estructuras de datos multiformato mediante ClipboardItem: tablas HTML con estilos para pegar en hojas de cálculo, gráficos PNG generados y representaciones en texto plano dentro de una única transacción atómica.
Al pulsar «Compartir», en lugar de cargar librerías de terceros con múltiples rastreadores para cada red social, se despliega la hoja de compartir nativa del sistema operativo (iOS/Android Share Sheet), permitiendo enviar el enlace, texto y archivos PDF directamente a AirDrop, WhatsApp o Telegram.
Comparemos los enfoques tradicionales frente a los estándares modernos W3C:
La consulta navigator.permissions.query({ name: 'camera' }) comprueba el estado ('granted', 'prompt', 'denied') antes de solicitar el hardware.
navigator.mediaDevices.getUserMedia(constraints) activa el sensor de vídeo con la resolución (1280x720) y tasa de refresco requeridas.
El flujo se asigna a videoElement.srcObject con autoPlay, playsInline y muted para evitar bucles de eco de audio.
Al desmontar el componente, la función cleanup invoca track.stop() en cada pista y establece srcObject en null.
Una aplicación de entrenamiento físico activó Screen Wake Lock para evitar que la pantalla se apague. El usuario minimiza temporalmente el navegador para responder un mensaje y regresa a la aplicación 45 segundos después. ¿Cuál es el estado del bloqueo de pantalla?
Implementaremos un subsistema completo y robusto para interactuar con las APIs de sistema:
src/hooks/useClipboard.ts: Hook para copiar texto y datos binarios (imágenes PNG) al portapapeles con fallback y temporizador de reinicio de estado.src/hooks/useWakeLock.ts: Hook reactivo para mantener la pantalla encendida con autorecuperación ante cambios de visibilidad de la pestaña.src/hooks/useMediaDevices.ts: Hook de control de cámara y micrófono con enumeración de dispositivos y parada garantizada de pistas al desmontar.src/components/media/HardwareControlStudio.tsx: Panel de control y diagnóstico previo de hardware con tipado estricto en TypeScript.'use client';
import * as React from 'react';
export interface UseClipboardOptions {
/** Duración en milisegundos del estado de copia activa */
resetDelayMs?: number;
}
export interface UseClipboardReturn {
copied: boolean;
copyText: (text: string) => Promise<boolean>;
copyBlob: (blob: Blob, mimeType?: string) => Promise<boolean>;
error: Error | null;
}
/**
* Hook de interacción segura con el portapapeles del sistema
*/
export function useClipboard(options: UseClipboardOptions = {}): UseClipboardReturn {
const { resetDelayMs = 2000 } = options;
const [copied, setCopied] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimer = React.useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const handleSuccess = React.useCallback(() => {
clearTimer();
setCopied(true);
setError(null);
timerRef.current = setTimeout(() => {
setCopied(false);
}, resetDelayMs);
}, [clearTimer, resetDelayMs]);
// 1. Copia de cadena de texto plano
const copyText = React.useCallback(
async (text: string): Promise<boolean> => {
clearTimer();
setError(null);
// Verificación de Async Clipboard API en Secure Context
if (typeof navigator !== 'undefined' && navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
handleSuccess();
return true;
} catch (err) {
const normError = err instanceof Error ? err : new Error(String(err));
setError(normError);
}
}
// Fallback mediante textarea oculto para entornos HTTP o navegadores antiguos
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
textArea.setAttribute('readonly', '');
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const success = document.execCommand('copy');
document.body.removeChild(textArea);
if (success) {
handleSuccess();
return true;
}
throw new Error('El fallback execCommand devolvió false');
} catch (fallbackErr) {
const normError = fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
setError(normError);
setCopied(false);
return false;
}
},
[clearTimer, handleSuccess]
);
// 2. Copia de objetos binarios (por ejemplo, capturas PNG)
const copyBlob = React.useCallback(
async (blob: Blob, mimeType: string = 'image/png'): Promise<boolean> => {
clearTimer();
setError(null);
if (typeof navigator === 'undefined' || !navigator.clipboard || !window.isSecureContext) {
const err = new Error('La copia de objetos binarios solo está admitida en Secure Context (HTTPS)');
setError(err);
return false;
}
try {
const item = new ClipboardItem({ [mimeType]: blob });
await navigator.clipboard.write([item]);
handleSuccess();
return true;
} catch (err) {
const normError = err instanceof Error ? err : new Error(String(err));
setError(normError);
return false;
}
},
[clearTimer, handleSuccess]
);
React.useEffect(() => {
return () => clearTimer();
}, [clearTimer]);
return { copied, copyText, copyBlob, error };
}'use client';
import * as React from 'react';
export interface UseWakeLockReturn {
isSupported: boolean;
isActive: boolean;
requestLock: () => Promise<boolean>;
releaseLock: () => Promise<void>;
error: Error | null;
}
/**
* Hook reactivo para mantener la pantalla activa con autorecuperación ante cambios de visibilidad
*/
export function useWakeLock(): UseWakeLockReturn {
const [isActive, setIsActive] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
const sentinelRef = React.useRef<WakeLockSentinel | null>(null);
const wantsActiveRef = React.useRef(false);
const isSupported =
typeof window !== 'undefined' &&
'wakeLock' in navigator &&
typeof navigator.wakeLock?.request === 'function';
const releaseLock = React.useCallback(async () => {
wantsActiveRef.current = false;
if (sentinelRef.current) {
try {
await sentinelRef.current.release();
} catch (err) {
console.warn('[WakeLock]: Error al liberar el token sentinel', err);
} finally {
sentinelRef.current = null;
setIsActive(false);
}
}
}, []);
const requestLock = React.useCallback(async (): Promise<boolean> => {
if (!isSupported) {
setError(new Error('Screen Wake Lock API no es compatible con este navegador'));
return false;
}
wantsActiveRef.current = true;
try {
const sentinel = await navigator.wakeLock.request('screen');
sentinelRef.current = sentinel;
sentinel.addEventListener('release', () => {
// Ejecutado si el SO o el navegador liberan el bloqueo (ej. cambio de pestaña)
sentinelRef.current = null;
setIsActive(false);
});
setIsActive(true);
setError(null);
return true;
} catch (err) {
const normError = err instanceof Error ? err : new Error(String(err));
setError(normError);
setIsActive(false);
return false;
}
}, [isSupported]);
// Recuperar el bloqueo cuando la página vuelve a estar visible
React.useEffect(() => {
if (!isSupported) return;
const handleVisibilityChange = async () => {
if (document.visibilityState === 'visible' && wantsActiveRef.current && !sentinelRef.current) {
await requestLock();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
releaseLock();
};
}, [isSupported, requestLock, releaseLock]);
return { isSupported, isActive, requestLock, releaseLock, error };
}'use client';
import * as React from 'react';
export interface MediaDevicesState {
stream: MediaStream | null;
isActive: boolean;
videoDevices: MediaDeviceInfo[];
audioDevices: MediaDeviceInfo[];
error: Error | null;
startCamera: (deviceId?: string) => Promise<boolean>;
stopCamera: () => void;
toggleAudioTrack: () => void;
toggleVideoTrack: () => void;
isAudioMuted: boolean;
isVideoMuted: boolean;
}
export function useMediaDevices(): MediaDevicesState {
const [stream, setStream] = React.useState<MediaStream | null>(null);
const [isActive, setIsActive] = React.useState(false);
const [videoDevices, setVideoDevices] = React.useState<MediaDeviceInfo[]>([]);
const [audioDevices, setAudioDevices] = React.useState<MediaDeviceInfo[]>([]);
const [error, setError] = React.useState<Error | null>(null);
const [isAudioMuted, setIsAudioMuted] = React.useState(false);
const [isVideoMuted, setIsVideoMuted] = React.useState(false);
// Parada determinista de las pistas físicas de hardware
const stopTracks = React.useCallback((mediaStream: MediaStream | null) => {
if (!mediaStream) return;
mediaStream.getTracks().forEach((track) => {
track.stop(); // Desconecta físicamente el hardware de la cámara/micrófono
});
}, []);
const stopCamera = React.useCallback(() => {
stopTracks(stream);
setStream(null);
setIsActive(false);
}, [stream, stopTracks]);
// Obtener la lista de dispositivos físicos conectados
const refreshDevices = React.useCallback(async () => {
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.enumerateDevices) {
return;
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
setVideoDevices(devices.filter((d) => d.kind === 'videoinput'));
setAudioDevices(devices.filter((d) => d.kind === 'audioinput'));
} catch (err) {
console.warn('[MediaDevices]: Error al enumerar dispositivos', err);
}
}, []);
const startCamera = React.useCallback(
async (deviceId?: string): Promise<boolean> => {
stopCamera();
setError(null);
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {
setError(new Error('MediaDevices API no está disponible en este entorno'));
return false;
}
try {
const constraints: MediaStreamConstraints = {
video: deviceId
? { deviceId: { exact: deviceId } }
: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' },
audio: true,
};
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
setStream(newStream);
setIsActive(true);
setIsAudioMuted(false);
setIsVideoMuted(false);
// Tras otorgar permisos, el navegador desvela los nombres reales de los dispositivos
await refreshDevices();
return true;
} catch (err) {
const normError = err instanceof Error ? err : new Error(String(err));
setError(normError);
setIsActive(false);
return false;
}
},
[stopCamera, refreshDevices]
);
const toggleAudioTrack = React.useCallback(() => {
if (!stream) return;
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
setIsAudioMuted(!audioTrack.enabled);
}
}, [stream]);
const toggleVideoTrack = React.useCallback(() => {
if (!stream) return;
const videoTrack = stream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
setIsVideoMuted(!videoTrack.enabled);
}
}, [stream]);
// Limpieza obligatoria al desmontar el componente
React.useEffect(() => {
refreshDevices();
return () => {
stopTracks(stream);
};
}, [stream, stopTracks, refreshDevices]);
return {
stream,
isActive,
videoDevices,
audioDevices,
error,
startCamera,
stopCamera,
toggleAudioTrack,
toggleVideoTrack,
isAudioMuted,
isVideoMuted,
};
}'use client';
import * as React from 'react';
import { useMediaDevices } from '@/hooks/useMediaDevices';
import { useWakeLock } from '@/hooks/useWakeLock';
import { useClipboard } from '@/hooks/useClipboard';
export function HardwareControlStudio() {
const {
stream,
isActive: isCamActive,
videoDevices,
startCamera,
stopCamera,
toggleAudioTrack,
toggleVideoTrack,
isAudioMuted,
isVideoMuted,
error: mediaError,
} = useMediaDevices();
const {
isSupported: isWakeSupported,
isActive: isWakeActive,
requestLock,
releaseLock,
} = useWakeLock();
const { copied, copyText } = useClipboard({ resetDelayMs: 2500 });
const [selectedDeviceId, setSelectedDeviceId] = React.useState<string>('');
const videoRef = React.useRef<HTMLVideoElement | null>(null);
// Vincular MediaStream al elemento video HTML5
React.useEffect(() => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
}, [stream]);
const handleShareDiagnostics = async () => {
const report = {
timestamp: new Date().toISOString(),
secureContext: typeof window !== 'undefined' ? window.isSecureContext : false,
cameraActive: isCamActive,
wakeLockActive: isWakeActive,
camerasCount: videoDevices.length,
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'Unknown',
};
const textReport = JSON.stringify(report, null, 2);
// Intento con Web Share API nativa; si se cancela o falla, fallback a portapapeles
if (typeof navigator !== 'undefined' && navigator.share) {
try {
await navigator.share({
title: 'Diagnóstico de hardware Fullstack Agentic Lab',
text: textReport,
});
return;
} catch {
// El usuario canceló la hoja de compartir — fallback al portapapeles
}
}
await copyText(textReport);
};
return (
<div className="p-6 rounded-2xl bg-bg-surface border border-border max-w-2xl mx-auto space-y-6 shadow-2xl">
{/* Cabecera */}
<div className="flex items-center justify-between border-b border-border pb-4">
<div>
<h3 className="font-bold text-text-primary text-lg">Panel de diagnóstico de hardware</h3>
<p className="text-xs text-text-secondary mt-0.5">MediaDevices, Screen Wake Lock y Clipboard APIs</p>
</div>
<div className="flex items-center gap-2">
<span
className={`px-2.5 py-1 rounded-full text-xs font-mono font-bold uppercase ${
isCamActive
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-bg-elevated text-text-muted border border-border'
}`}
>
{isCamActive ? 'Sensores activos' : 'Sensores apagados'}
</span>
</div>
</div>
{/* Visor de cámara */}
<div className="relative aspect-video w-full rounded-xl bg-black overflow-hidden border border-border flex items-center justify-center shadow-inner">
<video
ref={videoRef}
autoPlay
playsInline
muted
className={`w-full h-full object-cover transition-opacity duration-300 ${
isCamActive && !isVideoMuted ? 'opacity-100' : 'opacity-0'
}`}
/>
{(!isCamActive || isVideoMuted) && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-text-muted gap-2 bg-bg-surface/80 backdrop-blur-sm">
<span className="text-3xl">📷</span>
<span className="text-xs font-medium">
{isVideoMuted ? 'Vídeo silenciado por el usuario' : 'Cámara apagada'}
</span>
</div>
)}
</div>
{/* Controles de selección de dispositivo */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="camera-select" className="text-xs font-medium text-text-secondary">
Dispositivo de cámara:
</label>
<select
id="camera-select"
value={selectedDeviceId}
onChange={(e) => {
setSelectedDeviceId(e.target.value);
if (isCamActive) startCamera(e.target.value);
}}
className="w-full px-3 py-2 text-xs rounded-xl bg-bg-elevated border border-border text-text-primary focus:outline-none focus:border-accent-violet"
>
<option value="">Por defecto (Frontal)</option>
{videoDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label || `Cámara ${device.deviceId.slice(0, 5)}...`}
</option>
))}
</select>
</div>
{/* Control de Screen Wake Lock */}
<div className="space-y-2">
<span className="text-xs font-medium text-text-secondary block">Bloqueo de suspensión (Wake Lock):</span>
<button
type="button"
disabled={!isWakeSupported}
onClick={() => (isWakeActive ? releaseLock() : requestLock())}
className={`w-full px-3 py-2 text-xs font-semibold rounded-xl border transition-colors cursor-pointer flex items-center justify-center gap-2 ${
isWakeActive
? 'bg-amber-500/20 text-amber-300 border-amber-500/30'
: 'bg-bg-elevated text-text-secondary border-border hover:text-text-primary'
}`}
>
<span>{isWakeActive ? '☀️ Pantalla despierta (Activo)' : '🌙 Modo de reposo estándar'}</span>
</button>
</div>
</div>
{mediaError && (
<div className="p-3 rounded-xl bg-red-950/40 border border-red-900/50 text-xs text-red-300">
{mediaError.message}
</div>
)}
{/* Botones de acción */}
<div className="flex flex-wrap items-center justify-between gap-3 pt-2 border-t border-border">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => (isCamActive ? stopCamera() : startCamera(selectedDeviceId))}
className={`px-4 py-2 text-xs font-semibold rounded-xl transition-colors cursor-pointer shadow-md ${
isCamActive
? 'bg-red-600/20 text-red-400 border border-red-500/30 hover:bg-red-600/30'
: 'bg-accent-violet text-white hover:bg-accent-violet/90'
}`}
>
{isCamActive ? 'Desconectar hardware' : 'Iniciar cámara'}
</button>
{isCamActive && (
<>
<button
type="button"
onClick={toggleAudioTrack}
className="px-3 py-2 text-xs font-medium rounded-xl bg-bg-elevated border border-border text-text-secondary hover:text-text-primary transition-colors cursor-pointer"
>
{isAudioMuted ? '🔇 Activar micro' : '🎙️ Silenciar micro'}
</button>
<button
type="button"
onClick={toggleVideoTrack}
className="px-3 py-2 text-xs font-medium rounded-xl bg-bg-elevated border border-border text-text-secondary hover:text-text-primary transition-colors cursor-pointer"
>
{isVideoMuted ? '👁️ Mostrar vídeo' : '🙈 Ocultar vídeo'}
</button>
</>
)}
</div>
<button
type="button"
onClick={handleShareDiagnostics}
className="px-3.5 py-2 text-xs font-medium rounded-xl bg-bg-elevated border border-border text-text-secondary hover:text-text-primary transition-colors cursor-pointer flex items-center gap-1.5"
>
<span>{copied ? '✓ ¡Copiado!' : '📋 Compartir informe'}</span>
</button>
</div>
</div>
);
}Eliminar la etiqueta <video> del marcado no desactiva la cámara web. El flujo MediaStream continúa leyendo fotogramas del sensor, provocando sobrecalentamiento y un consumo acelerado de la batería.
Solución: En la función de limpieza del hook useEffect, invoque siempre stream.getTracks().forEach(t => t.stop()).
El modelo de seguridad del navegador exige Transient User Activation. Invocar navigator.share() o clipboard.writeText() dentro de un setTimeout, un intervalo de sondeo o sin un clic previo del usuario generará un error NotAllowedError: Must be handling a user gesture.
Solución: Llame a estos métodos de forma estrictamente síncrona en la raíz de manejadores de eventos como onClick o onSubmit.
En componentes de servidor de Next.js o durante el SSR, evaluar window, navigator.clipboard o navigator.mediaDevices en el cuerpo del componente provocará un fallo fatal en Node.js al no existir dichos objetos en el servidor.
Solución: Proteja el código con comprobaciones typeof window !== 'undefined', use la directiva 'use client' y sitúe la lógica de hardware en hooks useEffect.
El sistema operativo invalida automáticamente el token WakeLockSentinel cuando el usuario cambia de pestaña o minimiza el navegador. Si el componente asume erróneamente que el bloqueo sigue activo, la pantalla se apagará tras volver.
Solución: Escuche siempre el evento document.addEventListener('visibilitychange') y vuelva a solicitar navigator.wakeLock.request('screen') cuando document.visibilityState === 'visible'.
En entornos de prueba local a través de HTTP en dispositivos reales (por ejemplo, http://192.168.1.50:3000), navigator.mediaDevices y navigator.clipboard serán undefined.
Solución: Verifique el indicador window.isSecureContext antes de invocar las APIs y configure certificados HTTPS locales (usando herramientas como mkcert) para pruebas móviles en red local.
track.stop() en los hooks de React 19.https://), ejecución bajo Transient User Activation y auditoría no intrusiva mediante Permissions API.ClipboardItem (texto, HTML, PNG), superando las técnicas obsoletas de document.execCommand.