{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "ping-indicator",
	"title": "Ping Indicator",
	"type": "registry:ui",
	"description": "Four rising bars that show connection quality from a live probe.",
	"files": [
		{
			"content": "<script lang=\"ts\" module>\r\n\texport type PingStatus = 'excellent' | 'good' | 'fair' | 'poor' | 'offline' | 'idle';\r\n\r\n\texport type PingThresholds = {\r\n\t\t/** Below this many ms every bar lights up. */\r\n\t\texcellent: number;\r\n\t\tgood: number;\r\n\t\tfair: number;\r\n\t};\r\n\r\n\texport const PING_SIZES = {\r\n\t\tsm: { track: 'h-3 w-4', gap: 'gap-[1.5px]', text: 'text-[10px]' },\r\n\t\tmd: { track: 'h-4 w-5', gap: 'gap-[2px]', text: 'text-xs' },\r\n\t\tlg: { track: 'h-6 w-8', gap: 'gap-[3px]', text: 'text-sm' }\r\n\t} as const;\r\n\r\n\texport type PingSize = keyof typeof PING_SIZES;\r\n</script>\r\n\r\n<script lang=\"ts\">\r\n\timport { cn } from '$UTILS$';\r\n\timport { onMount, type Snippet } from 'svelte';\r\n\r\n\tlet {\r\n\t\turl,\r\n\t\tprobe,\r\n\t\tlatency = $bindable<number | null>(null),\r\n\t\tbars = 4,\r\n\t\tinterval = 3000,\r\n\t\ttimeout = 5000,\r\n\t\tsmoothing = 3,\r\n\t\tpaused = false,\r\n\t\tpauseWhenHidden = true,\r\n\t\tthresholds = { excellent: 80, good: 200, fair: 500 },\r\n\t\tsize = 'md',\r\n\t\tshowLatency = false,\r\n\t\tlabel,\r\n\t\tclass: className,\r\n\t\tonsample,\r\n\t\tchildren,\r\n\t\t...rest\r\n\t}: {\r\n\t\t/** Endpoint to time. Defaults to the current origin. Ignored when `probe` is set. */\r\n\t\turl?: string;\r\n\t\t/** Custom probe. Resolve with the round trip in ms, reject to show offline. */\r\n\t\tprobe?: () => Promise<number>;\r\n\t\t/** Latest round trip, or null when the last probe failed. */\r\n\t\tlatency?: number | null;\r\n\t\tbars?: number;\r\n\t\tinterval?: number;\r\n\t\ttimeout?: number;\r\n\t\t/** Number of recent samples to take the median of, so one blip cannot flicker the bars. */\r\n\t\tsmoothing?: number;\r\n\t\tpaused?: boolean;\r\n\t\tpauseWhenHidden?: boolean;\r\n\t\tthresholds?: PingThresholds;\r\n\t\tsize?: PingSize;\r\n\t\tshowLatency?: boolean;\r\n\t\tlabel?: string;\r\n\t\tclass?: string;\r\n\t\tonsample?: (ms: number | null) => void;\r\n\t\tchildren?: Snippet<[{ status: PingStatus; level: number; latency: number | null }]>;\r\n\t\t[key: string]: unknown;\r\n\t} = $props();\r\n\r\n\t// Deliberately not reactive: the polling effect reads it, so tracking it here\r\n\t// would make every probe re-run the effect.\r\n\tlet inFlight = false;\r\n\tlet hidden = $state(false);\r\n\tlet recent = $state<(number | null)[]>([]);\r\n\r\n\t/** Median of the recent window, ignoring failures unless every sample failed. */\r\n\tconst smoothed = $derived.by(() => {\r\n\t\tconst ok = recent.filter((v): v is number => v !== null);\r\n\t\tif (recent.length === 0) return undefined;\r\n\t\tif (ok.length === 0) return null;\r\n\r\n\t\tconst sorted = [...ok].sort((a, b) => a - b);\r\n\t\treturn sorted[Math.floor(sorted.length / 2)];\r\n\t});\r\n\r\n\tconst status = $derived.by<PingStatus>(() => {\r\n\t\tif (smoothed === undefined) return 'idle';\r\n\t\tif (smoothed === null) return 'offline';\r\n\t\tif (smoothed <= thresholds.excellent) return 'excellent';\r\n\t\tif (smoothed <= thresholds.good) return 'good';\r\n\t\tif (smoothed <= thresholds.fair) return 'fair';\r\n\t\treturn 'poor';\r\n\t});\r\n\r\n\t/** How many bars are lit, scaled to whatever `bars` is set to. */\r\n\tconst level = $derived.by(() => {\r\n\t\tconst ratio = { excellent: 1, good: 0.75, fair: 0.5, poor: 0.25, offline: 0, idle: 0 }[status];\r\n\t\treturn Math.round(ratio * bars);\r\n\t});\r\n\r\n\tconst STATUS_COLOR: Record<PingStatus, string> = {\r\n\t\texcellent: 'bg-emerald-500',\r\n\t\tgood: 'bg-emerald-500',\r\n\t\tfair: 'bg-amber-500',\r\n\t\tpoor: 'bg-orange-500',\r\n\t\toffline: 'bg-destructive',\r\n\t\tidle: 'bg-muted-foreground'\r\n\t};\r\n\r\n\tconst STATUS_TEXT: Record<PingStatus, string> = {\r\n\t\texcellent: 'text-emerald-500',\r\n\t\tgood: 'text-emerald-500',\r\n\t\tfair: 'text-amber-500',\r\n\t\tpoor: 'text-orange-500',\r\n\t\toffline: 'text-destructive',\r\n\t\tidle: 'text-muted-foreground'\r\n\t};\r\n\r\n\tconst STATUS_LABEL: Record<PingStatus, string> = {\r\n\t\texcellent: 'Excellent connection',\r\n\t\tgood: 'Good connection',\r\n\t\tfair: 'Fair connection',\r\n\t\tpoor: 'Poor connection',\r\n\t\toffline: 'Offline',\r\n\t\tidle: 'Checking connection'\r\n\t};\r\n\r\n\tconst dimensions = $derived(PING_SIZES[size]);\r\n\r\n\tfunction record(ms: number | null) {\r\n\t\tlatency = ms;\r\n\t\trecent = [...recent, ms].slice(-Math.max(1, smoothing));\r\n\t\tonsample?.(ms);\r\n\t}\r\n\r\n\tasync function defaultProbe() {\r\n\t\tconst target = url ?? (typeof window === 'undefined' ? '/' : window.location.origin);\r\n\t\tconst controller = new AbortController();\r\n\t\tconst timer = setTimeout(() => controller.abort(), timeout);\r\n\t\tconst started = performance.now();\r\n\r\n\t\ttry {\r\n\t\t\tawait fetch(`${target}${target.includes('?') ? '&' : '?'}_ping=${Date.now()}`, {\r\n\t\t\t\tmethod: 'HEAD',\r\n\t\t\t\tcache: 'no-store',\r\n\t\t\t\t// Opaque responses still carry accurate timing and avoid CORS failures.\r\n\t\t\t\tmode: 'no-cors',\r\n\t\t\t\tsignal: controller.signal\r\n\t\t\t});\r\n\t\t\treturn performance.now() - started;\r\n\t\t} finally {\r\n\t\t\tclearTimeout(timer);\r\n\t\t}\r\n\t}\r\n\r\n\texport async function ping() {\r\n\t\tif (inFlight) return;\r\n\t\tinFlight = true;\r\n\r\n\t\ttry {\r\n\t\t\tconst ms = await (probe ?? defaultProbe)();\r\n\t\t\trecord(Number.isFinite(ms) ? Math.round(ms) : null);\r\n\t\t} catch {\r\n\t\t\trecord(null);\r\n\t\t} finally {\r\n\t\t\tinFlight = false;\r\n\t\t}\r\n\t}\r\n\r\n\texport function reset() {\r\n\t\trecent = [];\r\n\t\tlatency = null;\r\n\t}\r\n\r\n\tconst running = $derived(!paused && !(pauseWhenHidden && hidden));\r\n\r\n\tonMount(() => {\r\n\t\tconst onVisibility = () => (hidden = document.visibilityState === 'hidden');\r\n\t\tonVisibility();\r\n\t\tdocument.addEventListener('visibilitychange', onVisibility);\r\n\t\treturn () => document.removeEventListener('visibilitychange', onVisibility);\r\n\t});\r\n\r\n\t$effect(() => {\r\n\t\tif (!running) return;\r\n\r\n\t\tping();\r\n\t\tconst timer = setInterval(ping, interval);\r\n\t\treturn () => clearInterval(timer);\r\n\t});\r\n</script>\r\n\r\n<span\r\n\tclass={cn('inline-flex items-center gap-1.5', className)}\r\n\trole=\"status\"\r\n\taria-live=\"off\"\r\n\ttitle={label ?? STATUS_LABEL[status]}\r\n\taria-label={`${label ? `${label}: ` : ''}${STATUS_LABEL[status]}${\r\n\t\tlatency === null ? '' : `, ${latency} ms`\r\n\t}`}\r\n\t{...rest}\r\n>\r\n\t{#if children}\r\n\t\t{@render children({ status, level, latency })}\r\n\t{:else}\r\n\t\t<span class={cn('inline-flex items-end', dimensions.track, dimensions.gap)} aria-hidden=\"true\">\r\n\t\t\t{#each Array.from({ length: bars }) as _, i}\r\n\t\t\t\t{@const lit = i < level}\r\n\t\t\t\t<span\r\n\t\t\t\t\tclass={cn(\r\n\t\t\t\t\t\t'flex-1 rounded-[1px] transition-colors duration-300',\r\n\t\t\t\t\t\tlit ? STATUS_COLOR[status] : 'bg-muted-foreground/25',\r\n\t\t\t\t\t\t// The tallest lit bar breathes while a probe is in flight.\r\n\t\t\t\t\t\tlit && i === level - 1 && running && 'animate-pulse'\r\n\t\t\t\t\t)}\r\n\t\t\t\t\tstyle:height=\"{((i + 1) / bars) * 100}%\"\r\n\t\t\t\t></span>\r\n\t\t\t{/each}\r\n\t\t</span>\r\n\r\n\t\t{#if status === 'offline'}\r\n\t\t\t<!-- Colour alone would not carry the state, so name it. -->\r\n\t\t\t<span class={cn('font-medium', dimensions.text, STATUS_TEXT.offline)}>offline</span>\r\n\t\t{:else if showLatency}\r\n\t\t\t<span class={cn('font-mono tabular-nums text-muted-foreground', dimensions.text)}>\r\n\t\t\t\t{latency === null ? '—' : `${latency} ms`}\r\n\t\t\t</span>\r\n\t\t{/if}\r\n\r\n\t\t{#if label && status !== 'offline' && !showLatency}\r\n\t\t\t<span class={cn('text-muted-foreground', dimensions.text)}>{label}</span>\r\n\t\t{/if}\r\n\t{/if}\r\n</span>\r\n",
			"type": "registry:ui",
			"target": "ping-indicator/ping-indicator.svelte"
		},
		{
			"content": "import PingIndicator from './ping-indicator.svelte';\r\n\r\nexport { PingIndicator, PingIndicator as Root };\r\nexport { PING_SIZES } from './ping-indicator.svelte';\r\nexport type { PingStatus, PingThresholds, PingSize } from './ping-indicator.svelte';\r\n",
			"type": "registry:ui",
			"target": "ping-indicator/index.ts"
		}
	]
}