{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "update-banner",
	"title": "Update Banner",
	"type": "registry:ui",
	"description": "A reload prompt that appears when a new service worker is waiting to take over.",
	"dependencies": [
		"@lucide/svelte@^0.554.0"
	],
	"devDependencies": [
		"@lucide/svelte@^0.544.0",
		"tailwind-variants@^3.1.1"
	],
	"registryDependencies": [
		"button"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\r\n\timport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\n\t/** Message a waiting service worker must handle to activate immediately. */\r\n\texport const SKIP_WAITING_MESSAGE = { type: 'SKIP_WAITING' };\r\n\r\n\texport const updateBannerVariants = tv({\r\n\t\tbase: 'z-50 flex items-start gap-3 rounded-lg border bg-popover p-3 text-sm text-popover-foreground shadow-lg',\r\n\t\tvariants: {\r\n\t\t\tposition: {\r\n\t\t\t\tinline: 'w-full',\r\n\t\t\t\t'bottom-right': 'fixed bottom-4 right-4 w-[min(22rem,calc(100vw-2rem))]',\r\n\t\t\t\t'bottom-left': 'fixed bottom-4 left-4 w-[min(22rem,calc(100vw-2rem))]',\r\n\t\t\t\t'bottom-center': 'fixed bottom-4 left-1/2 -translate-x-1/2 w-[min(28rem,calc(100vw-2rem))]',\r\n\t\t\t\t'top-center': 'fixed top-4 left-1/2 -translate-x-1/2 w-[min(28rem,calc(100vw-2rem))]'\r\n\t\t\t}\r\n\t\t},\r\n\t\tdefaultVariants: { position: 'bottom-right' }\r\n\t});\r\n\r\n\texport type UpdateBannerPosition = VariantProps<typeof updateBannerVariants>['position'];\r\n\texport type UpdateSource = 'service-worker' | 'manual';\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\timport { fly } from 'svelte/transition';\r\n\timport { Button } from '$lib/components/ui/button';\r\n\timport { RefreshCw, Sparkles, X } from '@lucide/svelte';\r\n\r\n\tlet {\r\n\t\tavailable = $bindable(false),\r\n\t\tsource = 'service-worker',\r\n\t\tscope,\r\n\t\tpollInterval = 0,\r\n\t\tposition = 'bottom-right',\r\n\t\ttitle = 'A new version is available',\r\n\t\tdescription = 'Reload to get the latest version of the app.',\r\n\t\treloadLabel = 'Reload',\r\n\t\tdismissLabel = 'Later',\r\n\t\tdismissible = true,\r\n\t\tautoReloadAfter,\r\n\t\treloadTimeout = 3000,\r\n\t\tclass: className,\r\n\t\tonreload,\r\n\t\tondismiss,\r\n\t\tonupdatefound,\r\n\t\ticon,\r\n\t\tchildren,\r\n\t\t...rest\r\n\t}: {\r\n\t\t/** Whether an update is waiting. Bindable so you can drive it yourself. */\r\n\t\tavailable?: boolean;\r\n\t\t/** `manual` skips all service worker wiring and only uses `available`. */\r\n\t\tsource?: UpdateSource;\r\n\t\t/** Service worker scope to look up. Defaults to the closest registration. */\r\n\t\tscope?: string;\r\n\t\t/** Milliseconds between `registration.update()` checks. 0 disables polling. */\r\n\t\tpollInterval?: number;\r\n\t\tposition?: UpdateBannerPosition;\r\n\t\ttitle?: string;\r\n\t\tdescription?: string;\r\n\t\treloadLabel?: string;\r\n\t\tdismissLabel?: string;\r\n\t\tdismissible?: boolean;\r\n\t\t/** Seconds to wait before reloading on its own. Omit to always ask first. */\r\n\t\tautoReloadAfter?: number;\r\n\t\t/** How long to wait for `controllerchange` before reloading anyway. */\r\n\t\treloadTimeout?: number;\r\n\t\tclass?: string;\r\n\t\t/** Return `false` to cancel the reload and just close the prompt. */\r\n\t\tonreload?: () => boolean | void;\r\n\t\tondismiss?: () => void;\r\n\t\tonupdatefound?: (worker: ServiceWorker) => void;\r\n\t\ticon?: Snippet;\r\n\t\tchildren?: Snippet;\r\n\t\t[key: string]: unknown;\r\n\t} = $props();\r\n\r\n\tlet registration: ServiceWorkerRegistration | null = null;\r\n\tlet waiting: ServiceWorker | null = null;\r\n\tlet reloading = $state(false);\r\n\tlet secondsLeft = $state<number | null>(null);\r\n\r\n\tfunction markAvailable(worker: ServiceWorker) {\r\n\t\twaiting = worker;\r\n\t\tavailable = true;\r\n\t\tonupdatefound?.(worker);\r\n\t}\r\n\r\n\t/** Checks a registration for a worker that has installed but not taken over yet. */\r\n\tfunction inspect(reg: ServiceWorkerRegistration) {\r\n\t\tif (reg.waiting && navigator.serviceWorker.controller) markAvailable(reg.waiting);\r\n\r\n\t\treg.addEventListener('updatefound', () => {\r\n\t\t\tconst installing = reg.installing;\r\n\t\t\tif (!installing) return;\r\n\r\n\t\t\tinstalling.addEventListener('statechange', () => {\r\n\t\t\t\t// Without an existing controller this is the very first install,\r\n\t\t\t\t// which is not an update the user needs to act on.\r\n\t\t\t\tif (installing.state === 'installed' && navigator.serviceWorker.controller) {\r\n\t\t\t\t\tmarkAvailable(installing);\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t});\r\n\t}\r\n\r\n\texport async function check() {\r\n\t\tawait registration?.update();\r\n\t}\r\n\r\n\texport async function reload() {\r\n\t\tif (reloading) return;\r\n\r\n\t\tif (onreload?.() === false) {\r\n\t\t\tavailable = false;\r\n\t\t\tsecondsLeft = null;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\treloading = true;\r\n\r\n\t\tif (!waiting) return window.location.reload();\r\n\r\n\t\t// Reload once the new worker has actually taken control, with a timeout so\r\n\t\t// a worker that ignores SKIP_WAITING cannot leave the app stuck.\r\n\t\tlet done = false;\r\n\t\tconst go = () => {\r\n\t\t\tif (done) return;\r\n\t\t\tdone = true;\r\n\t\t\twindow.location.reload();\r\n\t\t};\r\n\r\n\t\tnavigator.serviceWorker.addEventListener('controllerchange', go, { once: true });\r\n\t\tsetTimeout(go, reloadTimeout);\r\n\t\twaiting.postMessage(SKIP_WAITING_MESSAGE);\r\n\t}\r\n\r\n\tfunction dismiss() {\r\n\t\tavailable = false;\r\n\t\tsecondsLeft = null;\r\n\t\tondismiss?.();\r\n\t}\r\n\r\n\tonMount(() => {\r\n\t\tif (source !== 'service-worker' || typeof navigator === 'undefined') return;\r\n\t\tif (!('serviceWorker' in navigator)) return;\r\n\r\n\t\tlet poll: ReturnType<typeof setInterval> | undefined;\r\n\r\n\t\tnavigator.serviceWorker.getRegistration(scope).then((reg) => {\r\n\t\t\tif (!reg) return;\r\n\t\t\tregistration = reg;\r\n\t\t\tinspect(reg);\r\n\r\n\t\t\tif (pollInterval > 0) poll = setInterval(() => reg.update(), pollInterval);\r\n\t\t});\r\n\r\n\t\treturn () => clearInterval(poll);\r\n\t});\r\n\r\n\t// Countdown to an unattended reload.\r\n\t$effect(() => {\r\n\t\tif (!available || autoReloadAfter === undefined) {\r\n\t\t\tsecondsLeft = null;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tsecondsLeft = autoReloadAfter;\r\n\t\tconst timer = setInterval(() => {\r\n\t\t\tconst next = (secondsLeft ?? 0) - 1;\r\n\t\t\tsecondsLeft = next;\r\n\t\t\tif (next <= 0) {\r\n\t\t\t\tclearInterval(timer);\r\n\t\t\t\treload();\r\n\t\t\t}\r\n\t\t}, 1000);\r\n\r\n\t\treturn () => clearInterval(timer);\r\n\t});\r\n</script>\r\n\r\n{#if available}\r\n\t<div\r\n\t\trole=\"alert\"\r\n\t\taria-live=\"polite\"\r\n\t\ttransition:fly={{ y: position === 'top-center' ? -12 : 12, duration: 200 }}\r\n\t\tclass={cn(updateBannerVariants({ position }), className)}\r\n\t\t{...rest}\r\n\t>\r\n\t\t<span class=\"mt-0.5 flex shrink-0 text-primary\">\r\n\t\t\t{#if icon}\r\n\t\t\t\t{@render icon()}\r\n\t\t\t{:else}\r\n\t\t\t\t<Sparkles class=\"size-4\" />\r\n\t\t\t{/if}\r\n\t\t</span>\r\n\r\n\t\t<div class=\"min-w-0 flex-1\">\r\n\t\t\t{#if children}\r\n\t\t\t\t{@render children()}\r\n\t\t\t{:else}\r\n\t\t\t\t<p class=\"font-medium leading-none\">{title}</p>\r\n\t\t\t\t<p class=\"mt-1 text-xs text-muted-foreground\">\r\n\t\t\t\t\t{description}\r\n\t\t\t\t\t{#if secondsLeft !== null}\r\n\t\t\t\t\t\t<span class=\"font-mono\">Reloading in {secondsLeft}s.</span>\r\n\t\t\t\t\t{/if}\r\n\t\t\t\t</p>\r\n\t\t\t{/if}\r\n\r\n\t\t\t<div class=\"mt-3 flex items-center gap-2\">\r\n\t\t\t\t<Button size=\"sm\" onclick={reload} disabled={reloading}>\r\n\t\t\t\t\t<RefreshCw class={cn('size-3.5', reloading && 'animate-spin')} />\r\n\t\t\t\t\t{reloadLabel}\r\n\t\t\t\t</Button>\r\n\t\t\t\t{#if dismissible}\r\n\t\t\t\t\t<Button size=\"sm\" variant=\"ghost\" onclick={dismiss}>{dismissLabel}</Button>\r\n\t\t\t\t{/if}\r\n\t\t\t</div>\r\n\t\t</div>\r\n\r\n\t\t{#if dismissible}\r\n\t\t\t<button\r\n\t\t\t\ttype=\"button\"\r\n\t\t\t\tonclick={dismiss}\r\n\t\t\t\taria-label={dismissLabel}\r\n\t\t\t\tclass=\"shrink-0 rounded-sm p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\r\n\t\t\t>\r\n\t\t\t\t<X class=\"size-4\" />\r\n\t\t\t</button>\r\n\t\t{/if}\r\n\t</div>\r\n{/if}\r\n",
			"type": "registry:ui",
			"target": "update-banner/update-banner.svelte"
		},
		{
			"content": "import UpdateBanner, {\r\n\tupdateBannerVariants,\r\n\tSKIP_WAITING_MESSAGE,\r\n\ttype UpdateBannerPosition,\r\n\ttype UpdateSource\r\n} from './update-banner.svelte';\r\n\r\nexport {\r\n\tUpdateBanner,\r\n\tUpdateBanner as Root,\r\n\tupdateBannerVariants,\r\n\tSKIP_WAITING_MESSAGE,\r\n\ttype UpdateBannerPosition,\r\n\ttype UpdateSource\r\n};\r\n",
			"type": "registry:ui",
			"target": "update-banner/index.ts"
		}
	]
}