Package: @lookout/react v0.1.0
Peer Dependencies: React 18+ or 19+
Exports: ESM + CJS with TypeScript declarations
import { LookoutProvider, LookoutRecorder } from "@lookout/react";
function App() {
return (
<LookoutProvider token="your-64-char-hex-token" apiBaseUrl="https://lookout.hackclub.com">
<LookoutRecorder />
</LookoutProvider>
);
}For headless usage:
import { LookoutProvider, useLookout } from "@lookout/react";
function MyRecorder() {
const { state, actions } = useLookout();
return (
<div>
<p>Status: {state.status}</p>
<p>Time: {state.displaySeconds}s</p>
<button onClick={actions.startSharing}>Start</button>
<button onClick={actions.pause}>Pause</button>
<button onClick={() => actions.stop({ name: "My timelapse" })}>Stop</button>
</div>
);
}
function App() {
return (
<LookoutProvider token="..." apiBaseUrl="https://lookout.hackclub.com">
<MyRecorder />
</LookoutProvider>
);
}For camera (webcam) capture:
import { LookoutProvider, LookoutRecorder } from "@lookout/react";
function App() {
return (
<LookoutProvider
token="your-64-char-hex-token"
apiBaseUrl="https://lookout.hackclub.com"
capture={{ mode: "camera" }}
>
<LookoutRecorder />
</LookoutProvider>
);
}Context provider that configures the API client and settings for all child hooks/components.
<LookoutProvider
token="..."
apiBaseUrl="https://lookout.hackclub.com"
capture={{ intervalMs: 30000, jpegQuality: 0.9 }}
autoStart
>
{children}
</LookoutProvider>Props (LookoutProviderProps extends LookoutConfig):
| Prop | Type | Default | Description |
|---|---|---|---|
token |
TokenProvider |
required | Session token — string, sync getter, or async getter |
apiBaseUrl |
string |
"" (same origin) |
Server API base URL |
capture |
CaptureSettings |
See below | Screenshot capture settings |
retry |
RetrySettings |
See below | Upload retry/buffer settings |
callbacks |
LookoutCallbacks |
{} |
Lifecycle event callbacks |
statusPollIntervalMs |
number |
3000 |
Compilation status poll interval (ms) |
autoStart |
boolean |
false |
Auto-start screen sharing on mount |
appName |
string |
— | Host program embedding Lookout (e.g. "Fallout"). Reported in client telemetry as Lookout Sdk (Fallout)/<version> (…) and surfaced server-side as the session's clientInfo. |
children |
ReactNode |
required | Child components |
type TokenProvider =
| string // static token
| (() => string) // sync getter
| (() => Promise<string>); // async getter (e.g. fetch from your backend)| Field | Type | Default | Description |
|---|---|---|---|
intervalMs |
number |
60000 |
Screenshot interval in ms |
jpegQuality |
number |
0.85 |
JPEG quality (0–1) |
maxWidth |
number |
1920 |
Max capture width in px |
maxHeight |
number |
1080 |
Max capture height in px |
displayMediaConstraints |
DisplayMediaStreamOptions |
— | Override getDisplayMedia constraints |
mode |
CaptureMode |
"screen" |
Capture source: "screen" or "camera" |
camera |
CameraSettings |
{} |
Camera-specific settings (only used when mode is "camera") |
type CaptureMode = "screen" | "camera";| Field | Type | Default | Description |
|---|---|---|---|
deviceId |
string |
— | Preferred camera device ID (from enumerateDevices). Omit for default camera |
userMediaConstraints |
MediaTrackConstraints |
— | Additional getUserMedia video constraints (merged with defaults) |
| Field | Type | Default | Description |
|---|---|---|---|
maxRetries |
number |
3 |
Max retries per upload step |
retryDelays |
number[] |
[2000, 4000, 8000] |
Backoff delays per attempt (ms) |
maxPendingBuffer |
number |
5 |
Max screenshots buffered in memory |
Primary hook — composes all lower-level hooks and orchestrates the capture-upload loop. Must be used within <LookoutProvider>.
const { state, actions } = useLookout();Returns:
| Field | Type | Description |
|---|---|---|
status |
RecorderStatus |
Current status (see below) |
isSharing |
boolean |
Whether media capture is active (screen sharing or camera recording) |
isRecording |
boolean |
true when actively capturing (isSharing && (status === "active" || status === "pending")). Use this instead of compound checks in UI logic. |
trackedSeconds |
number |
Server-authoritative tracked time — max of session state and the value returned by the last upload confirmation. Updates per-upload, not just on poll. Bounded by what the server has credited; never inflated by client-side estimation. |
displaySeconds |
number |
Client-interpolated display time (ticks every second via RAF). Caps interpolation at one capture interval (60s) ahead of the last server credit, so stop/compile reveals at most a 60s drop. The baseRef ratchet defends against backward jumps on stale-read sync. |
screenshotCount |
number |
Number of confirmed screenshots — max of server count and local upload count, so it updates immediately on upload. |
uploads |
UploadState |
Upload queue: { pending, completed, failed } |
lastScreenshotUrl |
string | null |
Object URL of last captured screenshot |
videoUrl |
string | null |
Video URL when complete. Auto-fetched from server when status reaches "complete". |
error |
string | null |
Error message when status is "error" |
captureMode |
CaptureMode |
Active capture mode ("screen" or "camera") |
availableCameras |
MediaDeviceInfo[] |
Available camera devices (populated when mode is "camera") |
selectedCameraId |
string | null |
Currently selected camera device ID |
isPreviewing |
boolean |
Whether camera is in preview mode (stream live, capture loop not started). Camera mode only. |
previewStream |
MediaStream | null |
Live camera MediaStream for rendering in a <video> element. Available during preview and recording in camera mode. |
| Method | Signature | Description |
|---|---|---|
startSharing |
() => Promise<void> |
Start capture source and begin the capture-upload loop. In camera mode, reuses the preview stream if one is active. |
stopSharing |
() => void |
Stop capture source without stopping session (auto-pauses) |
pause |
() => Promise<void> |
Pause the session |
resume |
() => Promise<void> |
Resume a paused session |
stop |
(options?: { name?: string }) => Promise<void> |
Stop the session and trigger compilation. Optionally name the timelapse before stopping. |
selectCamera |
(deviceId: string) => void |
Select a camera device by ID. Works during preview and recording. |
startPreview |
() => Promise<void> |
Acquire camera stream for live preview without starting the capture loop. Camera mode only. |
stopPreview |
() => void |
Stop the preview stream. Camera mode only. |
Server states plus client-only states:
type RecorderStatus =
| "pending" // session created, not yet started
| "active" // recording in progress
| "paused" // paused by user or auto-pause
| "stopped" // stopped, waiting for compilation
| "compiling" // video being compiled
| "complete" // video ready
| "failed" // compilation failed
| "loading" // (client-only) initial session fetch
| "no-token" // (client-only) no token provided/resolved
| "error"; // (client-only) error stateHandles getDisplayMedia, canvas snapshots, and stream lifecycle. Can be used standalone (without provider) by passing explicit settings.
const { isSharing, startSharing, takeScreenshot, stopSharing } = useScreenCapture();Parameters:
| Param | Type | Description |
|---|---|---|
overrides |
CaptureSettings |
Optional overrides (merged with provider config) |
Returns:
| Field | Type | Description |
|---|---|---|
isSharing |
boolean |
Whether screen sharing is active |
startSharing |
() => Promise<void> |
Prompt user for screen sharing |
takeScreenshot |
() => Promise<CaptureResult | null> |
Capture current frame as JPEG blob |
stopSharing |
() => void |
Stop all media tracks |
interface CaptureResult {
blob: Blob; // JPEG image blob
width: number; // Pixel width
height: number; // Pixel height
capturedAtMs?: number; // Client-clock ms when the frame was grabbed
// (vs. when the upload eventually arrives).
// Forwarded as `capturedAt` to opt into credit
// mode; uploader falls back to Date.now() at
// enqueue if missing.
}Handles getUserMedia (webcam), device enumeration, canvas snapshots, and stream lifecycle. Supports a two-phase flow: preview (stream live, no capture loop) → recording (isSharing = true, triggers capture loop in useLookout). Can be used standalone (without provider) by passing explicit settings.
const {
isSharing, startSharing, takeScreenshot, stopSharing,
devices, selectedDeviceId, selectDevice,
isPreviewing, previewStream, startPreview, stopPreview,
} = useCameraCapture();Parameters:
| Param | Type | Description |
|---|---|---|
overrides |
CaptureSettings |
Optional overrides (merged with provider config) |
Returns:
| Field | Type | Description |
|---|---|---|
isSharing |
boolean |
Whether camera is recording (capture loop active) |
startSharing |
() => Promise<void> |
Start recording — reuses preview stream if active, otherwise acquires one |
takeScreenshot |
() => Promise<CaptureResult | null> |
Capture current frame as JPEG blob |
stopSharing |
() => void |
Stop recording and release camera stream |
devices |
MediaDeviceInfo[] |
Available camera devices (auto-updated on connect/disconnect) |
selectedDeviceId |
string | null |
Currently selected camera device ID |
selectDevice |
(deviceId: string) => void |
Switch to a different camera (restarts stream, preserves preview/recording mode) |
isPreviewing |
boolean |
Whether camera is in preview mode (stream live, not recording) |
previewStream |
MediaStream | null |
Live camera MediaStream — render in a <video> element for live preview |
startPreview |
() => Promise<void> |
Acquire camera stream for preview without starting the capture loop |
stopPreview |
() => void |
Stop preview and release camera stream |
Notes:
- Enumerates devices on mount and on the
devicechangeevent. - Safari may return devices with empty labels before first
getUserMediacall — labels are populated after the first stream is acquired. selectDevicewhile streaming will restart the stream with the new device, preserving the current mode (preview or recording).- Stream is cleaned up on unmount if the user navigates away without recording.
Runs the serial upload pipeline (getUploadUrl → R2 PUT → confirmScreenshot) end-to-end with per-leg retries. Must be used within <LookoutProvider>. The pre-0.2.4 fire-and-forget queue (enqueue/nextExpectedAt) was replaced with this synchronous form — matches the desktop Rust capture loop.
const { captureUploadConfirm, uploads, trackedSeconds, lastScreenshotUrl, lastError, sessionConflict, resetConflict } = useUploader();Returns:
| Field | Type | Description |
|---|---|---|
captureUploadConfirm |
(capture: CaptureResult) => Promise<{ trackedSeconds, nextExpectedAt }> |
Run the full pipeline. Resolves with the fresh tracked_seconds and next_expected_at from THIS capture's confirm response. Throws after retries — caller (the tick chain) catches and falls back to the local interval. |
uploads |
UploadState |
{ pending, completed, failed } counts. Note: completed reflects successful confirms, not credited captures — a confirm can succeed with credited_seconds = 0 in credit mode. |
trackedSeconds |
number |
Server-reported tracked time from the last successful confirm |
lastScreenshotUrl |
string | null |
Object URL of last uploaded screenshot |
lastError |
string | null |
Last upload error message |
sessionConflict |
boolean |
true when a 409 was received (session paused/stopped server-side) |
resetConflict |
() => void |
Clear the sessionConflict flag after handling |
Manages session state, status polling, and server interactions. Must be used within <LookoutProvider>.
const session = useSession();Returns:
| Field | Type | Description |
|---|---|---|
status |
RecorderStatus |
Current session status |
name |
string |
Timelapse name |
trackedSeconds |
number |
Server-tracked seconds |
screenshotCount |
number |
Confirmed screenshot count |
startedAt |
string | null |
Session start timestamp |
createdAt |
string | null |
Session creation timestamp |
totalActiveSeconds |
number |
Accumulated active time |
error |
string | null |
Error message |
pause |
() => Promise<void> |
Pause the session |
resume |
() => Promise<void> |
Resume the session |
stop |
(name?: string) => Promise<void> |
Stop the session. Optionally name the timelapse before stopping (non-fatal if rename fails). |
reload |
() => Promise<void> |
Re-fetch session from server |
syncStatus |
() => Promise<void> |
Best-effort fetch of latest server status (used when a 409 surfaces in the uploader to reconcile local state) |
updateTrackedSeconds |
(seconds: number) => void |
Update tracked seconds locally |
setError |
(error: string | null) => void |
Set error state |
Client-side interpolated timer. Uses the server-provided seconds as ground truth, ticks every second via requestAnimationFrame, and caps interpolation at one capture interval (60s) ahead of the last server-credited value. Maintains a monotonic ratchet so a stale-read sync returning a lower value doesn't make the display jump backward.
When a new serverTrackedSeconds arrives, baseRef ratchets to max(baseRef, serverTrackedSeconds) and the elapsed-since-sync clock resets. Between credits, the displayed value is baseRef + min(60, elapsed_seconds) — so if captures stall, the display freezes 60s ahead of the last credit instead of running unbounded. When the next credit lands, the new server value equals the previously-frozen display (no visible jump).
When isActive flips to false (pause/stop/compile), the display snaps to baseRef — the maximum possible drop the user sees is 60s, never the full session length.
Important: feed this hook only server-authoritative values. Do not derive a synthetic value from
uploads.completed— in credit mode, not every successful upload credits a minute, so derived counts inflate the display.useLookoutalready enforces this viacomputeBestTrackedSeconds.
const displaySeconds = useSessionTimer(trackedSeconds, isActive);Parameters:
| Param | Type | Description |
|---|---|---|
serverTrackedSeconds |
number |
Server-authoritative tracked time (from state.trackedSeconds, an upload-confirm response, or a status poll) |
isActive |
boolean |
Whether to tick the timer. Set to false on pause/stop/compile to snap display to the server value |
Returns: number — display seconds (baseRef + capped interpolated elapsed while active; baseRef when inactive)
Manages session tokens in localStorage with cross-tab sync. No provider required.
const store = useTokenStore();Returns (UseTokenStore):
| Field | Type | Description |
|---|---|---|
tokens |
TokenEntry[] |
Active (non-archived) tokens |
archivedTokens |
TokenEntry[] |
Archived tokens |
addToken |
(token: string, label?: string) => void |
Add a token |
archiveToken |
(token: string) => void |
Archive a token |
unarchiveToken |
(token: string) => void |
Unarchive a token |
removeToken |
(token: string) => void |
Permanently remove a token |
getAllTokenValues |
() => string[] |
Get all active token strings |
hasToken |
(token: string) => boolean |
Check if a token exists |
interface TokenEntry {
token: string;
addedAt: string; // ISO timestamp
label?: string;
archived: boolean;
}Storage key: lookout-tokens
Fetches multiple sessions for gallery display via the batch endpoint. Auto-refreshes on tab focus. No provider required.
const { sessions, loading, error, refresh } = useGallery({
apiBaseUrl: "https://lookout.hackclub.com",
tokens: ["token1", "token2"],
});Parameters (UseGalleryOptions):
| Field | Type | Description |
|---|---|---|
apiBaseUrl |
string |
Server API base URL |
tokens |
string[] |
Token strings to fetch |
Returns (UseGallery):
| Field | Type | Description |
|---|---|---|
sessions |
SessionSummary[] |
Fetched sessions (newest first) |
loading |
boolean |
Whether fetch is in progress |
error |
string | null |
Fetch error message |
refresh |
() => void |
Manually re-fetch |
Simple hash-based router for single-page app navigation. No provider required.
const { route, navigate } = useHashRouter();Returns:
| Field | Type | Description |
|---|---|---|
route |
Route |
Current route |
navigate |
(route: Route) => void |
Navigate to a route |
type Route =
| { page: "gallery" } // #/
| { page: "record"; token: string } // #/record?token=...
| { page: "session"; token: string }; // #/session?token=...Drop-in recorder widget. Handles the full lifecycle: capture, upload, pause/resume/stop, compilation polling, and video display. Adapts its UI based on the configured capture.mode. Must be used within <LookoutProvider>.
<LookoutRecorder />No props — reads everything from context.
Renders based on status:
loading— spinnerno-token— "no session token" messageerror— error displaystopped/compiling/complete/failed—<ProcessingState>pending/active/paused— capture UI (varies by mode, see below)
Screen mode (capture.mode: "screen", default):
<StatusBar>+<ScreenPreview>+<RecordingControls>- Copy: "Share Screen & Start Recording", "Share Screen & Resume"
Camera mode (capture.mode: "camera"):
- Three-phase flow: idle → preview → recording
- Idle: "Start Camera" button (acquires camera stream for preview)
- Preview:
<CameraPreview>(live video) +<CameraSelector>(if multiple cameras) + "Start Recording" / "Cancel" - Recording:
<CameraPreview>+ standard Pause/Stop controls - Copy adapts: "Start Camera", "Start Recording", "Start Camera & Resume"
Displays timer, screenshot count, and upload queue status.
<StatusBar displaySeconds={120} screenshotCount={5} uploads={{ pending: 1, completed: 4, failed: 0 }} />Props (StatusBarProps):
| Prop | Type | Description |
|---|---|---|
displaySeconds |
number |
Seconds to display (formatted as H:MM:SS) |
screenshotCount |
number |
Confirmed screenshot count |
uploads |
UploadState |
{ pending, completed, failed } |
Action buttons for start/pause/resume/stop, adapts to current state.
<RecordingControls
status="active"
isSharing={true}
onStartSharing={() => {}}
onPause={() => {}}
onResume={() => {}}
onStop={() => {}}
/>Props (RecordingControlsProps):
| Prop | Type | Description |
|---|---|---|
status |
RecorderStatus |
Current session status |
isSharing |
boolean |
Whether screen sharing is active |
onStartSharing |
() => void |
Start screen sharing callback |
onPause |
() => void |
Pause callback |
onResume |
() => void |
Resume callback |
onStop |
() => void |
Stop callback |
loading |
boolean? |
Show loading state on buttons |
Displays the last captured screenshot. Renders nothing if no image.
<ScreenPreview imageUrl={lastScreenshotUrl} />Props (ScreenPreviewProps):
| Prop | Type | Description |
|---|---|---|
imageUrl |
string | null |
Object URL of the screenshot |
Live camera preview using a <video> element. Falls back to a static image when no stream is provided. Mirrors the video horizontally for a natural selfie-view.
<CameraPreview stream={state.previewStream} fallbackImageUrl={state.lastScreenshotUrl} />Props (CameraPreviewProps):
| Prop | Type | Description |
|---|---|---|
stream |
MediaStream | null |
Live camera MediaStream to display |
fallbackImageUrl |
string | null? |
Fallback static image URL (e.g. last captured screenshot) |
Camera device picker dropdown. Renders nothing if no devices are available.
<CameraSelector
devices={state.availableCameras}
selectedDeviceId={state.selectedCameraId}
onSelect={actions.selectCamera}
disabled={state.isSharing}
/>Props (CameraSelectorProps):
| Prop | Type | Description |
|---|---|---|
devices |
MediaDeviceInfo[] |
Available camera devices |
selectedDeviceId |
string | null |
Currently selected device ID |
onSelect |
(deviceId: string) => void |
Device selection callback |
disabled |
boolean? |
Disable selection (e.g., while recording) |
Displays compilation progress, video player, or failure state.
<ProcessingState status="compiling" trackedSeconds={300} />
<ProcessingState status="complete" trackedSeconds={300} videoUrl="https://..." />Props (ProcessingStateProps):
| Prop | Type | Description |
|---|---|---|
status |
string |
Session status |
trackedSeconds |
number |
Tracked time to display |
videoUrl |
string? |
Video URL (shown when complete) |
error |
string? |
Error message |
onVideoLoaded |
() => void? |
Callback when video element loads |
Wraps <ProcessingState> with automatic video URL fetching from the API. Must be used within <LookoutProvider>.
<ResultView status="complete" trackedSeconds={300} />Props (ResultViewProps):
| Prop | Type | Description |
|---|---|---|
status |
RecorderStatus |
Session status |
trackedSeconds |
number |
Tracked time |
Grid of session cards with loading, empty, and error states.
<Gallery
sessions={sessions}
loading={false}
error={null}
onSessionClick={(token) => navigate({ page: "session", token })}
onArchive={(token) => store.archiveToken(token)}
onRefresh={refresh}
/>Props (GalleryProps):
| Prop | Type | Description |
|---|---|---|
sessions |
SessionSummary[] |
Sessions to display |
loading |
boolean |
Show skeleton loader |
error |
string | null |
Error message |
onSessionClick |
(token: string) => void? |
Card click handler |
onArchive |
(token: string) => void? |
Archive button handler |
onRefresh |
() => void? |
Refresh button handler |
Individual session card with thumbnail, status badge, timelapse name, tracked time, and recording date.
<SessionCard session={session} onClick={() => {}} onArchive={() => {}} />Props (SessionCardProps):
| Prop | Type | Description |
|---|---|---|
session |
SessionSummary |
Session data |
onClick |
() => void? |
Click handler |
onArchive |
() => void? |
Archive button handler |
Full session detail view with video player, stats, and compilation polling. Standalone (no provider needed).
<SessionDetail
token="..."
apiBaseUrl="https://lookout.hackclub.com"
onBack={() => navigate({ page: "gallery" })}
onArchive={() => store.archiveToken(token)}
/>Props (SessionDetailProps):
| Prop | Type | Description |
|---|---|---|
token |
string |
Session token |
apiBaseUrl |
string |
Server API base URL |
onBack |
() => void? |
Back button handler |
onArchive |
() => void? |
Archive button handler |
Pass via LookoutProvider's callbacks prop:
<LookoutProvider
token="..."
callbacks={{
onShareStart: () => console.log("sharing started"),
onCapture: (capture) => console.log("captured", capture.width, "x", capture.height),
onUploadSuccess: ({ screenshotId, trackedSeconds }) => {},
onUploadFailure: (error) => {},
onPause: ({ totalActiveSeconds }) => {},
onResume: () => {},
onStop: ({ trackedSeconds, totalActiveSeconds }) => {},
onComplete: ({ videoUrl }) => {},
onCompilationFailed: () => {},
onError: (error, context) => {},
onStatusChange: (prev, next) => {},
}}
>| Callback | Arguments | When |
|---|---|---|
onShareStart |
— | Screen sharing started |
onShareStop |
— | Screen sharing ended |
onCapture |
CaptureResult |
Screenshot captured (before upload) |
onUploadSuccess |
{ screenshotId, trackedSeconds } |
Screenshot uploaded and confirmed |
onUploadFailure |
Error |
Upload failed after all retries |
onPause |
{ totalActiveSeconds } |
Session paused |
onResume |
— | Session resumed |
onStop |
{ trackedSeconds, totalActiveSeconds } |
Session stopped |
onComplete |
{ videoUrl } |
Compilation complete, video ready |
onCompilationFailed |
— | Compilation failed |
onError |
(Error, context: string) |
Any non-fatal error |
onStatusChange |
(prev, next) |
Status transition |
Standalone API client with no React dependency. Useful for server-side or non-React contexts.
import { createLookoutClient } from "@lookout/react";
const client = createLookoutClient({
baseUrl: "https://lookout.hackclub.com",
token: "your-token",
});
const session = await client.getSession();Options (CreateClientOptions):
| Field | Type | Description |
|---|---|---|
baseUrl |
string |
Server API base URL |
token |
TokenProvider |
Session token (string, sync, or async getter) |
Returns (LookoutClient):
| Method | Signature | Description |
|---|---|---|
resolveToken |
() => Promise<string> |
Resolve the token value |
getSession |
() => Promise<SessionResponse> |
Fetch session status |
getUploadUrl |
() => Promise<UploadUrlResponse> |
Get presigned upload URL |
confirmScreenshot |
(body) => Promise<ConfirmScreenshotResponse> |
Confirm upload |
uploadToR2 |
(uploadUrl, blob) => Promise<void> |
PUT blob to presigned URL |
pause |
() => Promise<PauseResponse> |
Pause session |
resume |
() => Promise<ResumeResponse> |
Resume session |
stop |
() => Promise<StopResponse> |
Stop session |
rename |
(name: string) => Promise<RenameSessionResponse> |
Rename the timelapse |
getStatus |
() => Promise<StatusResponse> |
Poll compilation status |
getVideo |
() => Promise<VideoResponse> |
Get video URL |
The SDK exports styled UI primitives used by its components. All use inline styles (no CSS imports needed).
| Export | Description |
|---|---|
Button |
Styled button with variants: primary, secondary, success, warning, danger, ghost and sizes: sm, md, lg |
Spinner |
Loading spinner with sizes: sm, md, lg |
Badge |
Status badge with variants: default, overlay |
Card |
Styled card container |
ErrorDisplay |
Error message display with variants: inline, banner, page |
PageContainer |
Page layout wrapper |
Skeleton / GallerySkeleton / SessionDetailSkeleton / RecordPageSkeleton |
Loading skeletons |
colors / spacing / radii / fontSize / fontWeight / statusConfig |
Theme tokens |
Formats seconds as H:MM:SS or M:SS. Used for the live timer display.
import { formatTime } from "@lookout/react";
formatTime(0); // "0:00"
formatTime(65); // "1:05"
formatTime(3661); // "1:01:01"Formats seconds as human-readable tracked time. Used for static time displays (gallery cards, stats) where second-level precision is unnecessary.
import { formatTrackedTime } from "@lookout/react";
formatTrackedTime(0); // "< 1min"
formatTrackedTime(300); // "5min"
formatTrackedTime(5640); // "1h 34min"
formatTrackedTime(7200); // "2h"