/**
* ProgressBar — determinate or indeterminate progress with optional chrome.
* Uses Tamagui stacks/tokens only (no raw CSS gradients) for consistent theming and RN compatibility.
*
* Slots (all optional): `header`, `footer` (e.g. wrap your own ScrollView for history), `leftSlot` / `rightSlot`
* for inline actions, `label` centered above the track in the main column.
*
* @typedef {'determinate' | 'indeterminate'} ProgressMode
* @typedef {'inline' | 'fixedTop'} ProgressVariant
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Text, XStack, YStack } from 'tamagui';
import { scheduleInterval, scheduleTimeout } from '../../platform/compat.js';
function clamp01(n) {
const x = Number(n);
if (!Number.isFinite(x)) {
return 0;
}
return Math.max(0, Math.min(100, x));
}
function renderNode(node) {
if (node === null || node === undefined || node === false) {
return null;
}
return node;
}
function hasSlot(node) {
return node !== null && node !== undefined && node !== false;
}
export function ProgressBar({
mode = 'indeterminate',
value = 0,
active = false,
/** Delay before unmount after `active` becomes false (smooth fade-out). Inline only; `fixedTop` hides immediately to avoid layout jump (label removed) during opacity animation. */
retainAfterInactiveMs = 420,
label,
header,
footer,
leftSlot,
rightSlot,
variant = 'inline',
trackHeight = 4,
zIndex = 20000
}) {
const [offset, setOffset] = useState(-45);
const [mounted, setMounted] = useState(active);
/** `fixedTop` follows `active` only — no delayed unmount, so the track cannot re-anchor to the top when the parent clears `label` on the same tick as `active`. */
const useRetainAfterInactive = variant !== 'fixedTop';
const visible = useRetainAfterInactive ? mounted : active;
const determinateWidth = useMemo(() => `${clamp01(value)}%`, [value]);
useEffect(() => {
if (active) {
setMounted(true);
}
}, [active]);
useEffect(() => {
if (!visible || mode !== 'indeterminate') {
return undefined;
}
return scheduleInterval(() => {
setOffset((v) => (v >= 100 ? -45 : v + 4));
}, 80);
}, [visible, mode]);
useEffect(() => {
if (!useRetainAfterInactive || active || !mounted) {
return undefined;
}
return scheduleTimeout(() => {
setMounted(false);
}, retainAfterInactiveMs);
}, [active, mounted, retainAfterInactiveMs, useRetainAfterInactive]);
if (!visible) {
return null;
}
const track = (
{mode === 'determinate' ? (
) : (
)}
);
const centerBlock = (
{label !== null && label !== undefined && label !== '' ? (
typeof label === 'string' ? (
{label}
) : (
label
)
) : null}
{track}
);
const body = (
{hasSlot(header) ? {renderNode(header)} : null}
{hasSlot(leftSlot) ? (
{renderNode(leftSlot)}
) : null}
{centerBlock}
{hasSlot(rightSlot) ? (
{renderNode(rightSlot)}
) : null}
{hasSlot(footer) ? (
{renderNode(footer)}
) : null}
);
if (variant === 'fixedTop') {
return (
{body}
);
}
return {body};
}
export default ProgressBar;