{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "audio-player",
	"title": "Audio Player",
	"type": "registry:ui",
	"description": "A compound audio player with play/pause, progress scrubbing, duration, time, and playback-speed controls.",
	"dependencies": [
		"@lucide/svelte",
		"bits-ui"
	],
	"devDependencies": [
		"@lucide/svelte@^1.7.0",
		"bits-ui@^2.18.0"
	],
	"registryDependencies": [
		"button",
		"dropdown-menu"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\n\timport type { Snippet } from \"svelte\";\n\n\texport type AudioPlayerProps = {\n\t\t/**\n\t\t * Sub-components that read the shared player state via context\n\t\t * (e.g. `<AudioPlayer.Button />`, `<AudioPlayer.Progress />`).\n\t\t */\n\t\tchildren?: Snippet;\n\t};\n</script>\n\n<script lang=\"ts\">\n\timport { setAudioPlayer } from \"./context.svelte.js\";\n\n\tlet { children }: AudioPlayerProps = $props();\n\n\tconst player = setAudioPlayer();\n\n\tlet audioEl: HTMLAudioElement | null = $state(null);\n\n\t$effect(() => {\n\t\tplayer.audio = audioEl;\n\t});\n\n\t$effect(() => {\n\t\tlet raf: number | null = null;\n\t\tconst tick = () => {\n\t\t\tconst el = player.audio;\n\t\t\tif (el) {\n\t\t\t\tplayer.time = el.currentTime;\n\t\t\t\tplayer.readyState = el.readyState;\n\t\t\t\tplayer.networkState = el.networkState;\n\t\t\t\tplayer.paused = el.paused;\n\t\t\t\tplayer.error = el.error;\n\t\t\t\tplayer.playbackRate = el.playbackRate;\n\t\t\t\tconst d = el.duration;\n\t\t\t\tif (Number.isFinite(d) && player.duration !== d) {\n\t\t\t\t\tplayer.duration = d;\n\t\t\t\t}\n\t\t\t}\n\t\t\traf = requestAnimationFrame(tick);\n\t\t};\n\t\traf = requestAnimationFrame(tick);\n\t\treturn () => {\n\t\t\tif (raf !== null) cancelAnimationFrame(raf);\n\t\t};\n\t});\n</script>\n\n<audio\n\tbind:this={audioEl}\n\tdata-slot=\"audio-player\"\n\tondurationchange={(e) => {\n\t\tconst d = e.currentTarget.duration;\n\t\tplayer.duration = Number.isFinite(d) ? d : undefined;\n\t}}\n\tonloadedmetadata={(e) => {\n\t\tconst d = e.currentTarget.duration;\n\t\tplayer.duration = Number.isFinite(d) ? d : undefined;\n\t}}\n\tonplay={() => (player.paused = false)}\n\tonpause={() => (player.paused = true)}\n\tonerror={() => (player.error = audioEl?.error ?? null)}\n\tclass=\"hidden\"\n\tcrossorigin=\"anonymous\"\n></audio>\n{@render children?.()}\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport type { ComponentProps } from \"svelte\";\n\timport Pause from \"@lucide/svelte/icons/pause\";\n\timport Play from \"@lucide/svelte/icons/play\";\n\timport { Button } from \"../button/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { useAudioPlayer, type AudioPlayerItem } from \"./context.svelte.js\";\n\n\ttype Props = ComponentProps<typeof Button> & {\n\t\titem?: AudioPlayerItem;\n\t\tonclick?: (e: MouseEvent) => void;\n\t};\n\n\tlet {\n\t\titem,\n\t\tclass: className,\n\t\tonclick: externalOnClick,\n\t\tchildren,\n\t\t...restProps\n\t}: Props = $props();\n\n\tconst player = useAudioPlayer();\n\n\tconst playing = $derived(\n\t\titem ? player.isItemActive(item.id) && player.isPlaying : player.isPlaying\n\t);\n\tconst loading = $derived(\n\t\titem\n\t\t\t? player.isItemActive(item.id) && player.isBuffering && player.isPlaying\n\t\t\t: player.isBuffering && player.isPlaying\n\t);\n</script>\n\n<Button\n\ttype=\"button\"\n\taria-label={playing ? \"Pause\" : \"Play\"}\n\tclass={cn(\"relative\", className)}\n\tonclick={(e) => {\n\t\tconst shouldPlay = !playing;\n\t\tif (shouldPlay) {\n\t\t\tif (item) {\n\t\t\t\tvoid player.play(item);\n\t\t\t} else {\n\t\t\t\tvoid player.play();\n\t\t\t}\n\t\t} else {\n\t\t\tvoid player.pause();\n\t\t}\n\t\texternalOnClick?.(e);\n\t}}\n\t{...restProps}\n>\n\t{#if playing}\n\t\t<Pause class={cn(\"size-4\", loading && \"opacity-0\")} aria-hidden=\"true\" />\n\t{:else}\n\t\t<Play class={cn(\"size-4\", loading && \"opacity-0\")} aria-hidden=\"true\" />\n\t{/if}\n\t{#if loading}\n\t\t<div\n\t\t\tclass=\"absolute inset-0 flex items-center justify-center rounded-[inherit] backdrop-blur-xs\"\n\t\t>\n\t\t\t<div\n\t\t\t\tclass=\"border-muted border-t-foreground size-3.5 animate-spin rounded-full border-2\"\n\t\t\t\trole=\"status\"\n\t\t\t\taria-label=\"Loading\"\n\t\t\t>\n\t\t\t\t<span class=\"sr-only\">Loading...</span>\n\t\t\t</div>\n\t\t</div>\n\t{/if}\n\t{@render children?.()}\n</Button>\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player-button.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { useAudioPlayer } from \"./context.svelte.js\";\n\timport { formatTime } from \"./utils.js\";\n\n\tlet { class: className, ...restProps }: HTMLAttributes<HTMLSpanElement> = $props();\n\n\tconst player = useAudioPlayer();\n\n\tconst display = $derived(\n\t\tplayer.duration !== undefined &&\n\t\t\tNumber.isFinite(player.duration) &&\n\t\t\t!Number.isNaN(player.duration)\n\t\t\t? formatTime(player.duration)\n\t\t\t: \"--:--\"\n\t);\n</script>\n\n<span\n\tdata-slot=\"audio-player-duration\"\n\tclass={cn(\"text-muted-foreground text-sm tabular-nums\", className)}\n\t{...restProps}\n>\n\t{display}\n</span>\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player-duration.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { Slider as SliderPrimitive } from \"bits-ui\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { useAudioPlayer } from \"./context.svelte.js\";\n\n\ttype Props = Omit<\n\t\tSliderPrimitive.RootProps,\n\t\t\"type\" | \"value\" | \"min\" | \"max\" | \"onValueChange\" | \"onValueCommit\" | \"children\" | \"child\"\n\t> & {\n\t\tstep?: number;\n\t\tonValueChange?: (value: number) => void;\n\t};\n\n\tlet {\n\t\tclass: className,\n\t\tstep = 0.25,\n\t\tonValueChange: externalOnValueChange,\n\t\tonpointerdown: externalPointerDown,\n\t\tonpointerup: externalPointerUp,\n\t\tonkeydown: externalKeyDown,\n\t\t...restProps\n\t}: Props = $props();\n\n\tconst player = useAudioPlayer();\n\n\tconst disabled = $derived(\n\t\tplayer.duration === undefined ||\n\t\t\t!Number.isFinite(player.duration) ||\n\t\t\tNumber.isNaN(player.duration)\n\t);\n\tconst max = $derived(disabled ? 0 : (player.duration as number));\n\tconst currentValue = $derived(Math.min(player.time, max));\n\n\tlet wasPlaying = false;\n\tlet userInteracting = false;\n</script>\n\n<SliderPrimitive.Root\n\ttype=\"single\"\n\tdata-slot=\"audio-player-progress\"\n\tmin={0}\n\t{max}\n\t{step}\n\t{disabled}\n\tvalue={currentValue}\n\tonValueChange={(v) => {\n\t\tif (!userInteracting) return;\n\t\tplayer.seek(v);\n\t\texternalOnValueChange?.(v);\n\t}}\n\tonpointerdown={(e) => {\n\t\tuserInteracting = true;\n\t\twasPlaying = player.isPlaying;\n\t\tvoid player.pause();\n\t\texternalPointerDown?.(e);\n\t}}\n\tonpointerup={(e) => {\n\t\tuserInteracting = false;\n\t\tif (wasPlaying) void player.play();\n\t\texternalPointerUp?.(e);\n\t}}\n\tonkeydown={(e) => {\n\t\tif (e.key === \" \") {\n\t\t\te.preventDefault();\n\t\t\tif (player.isPlaying) void player.pause();\n\t\t\telse void player.play();\n\t\t} else {\n\t\t\tuserInteracting = true;\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tuserInteracting = false;\n\t\t\t});\n\t\t}\n\t\texternalKeyDown?.(e);\n\t}}\n\tclass={cn(\n\t\t\"group/player relative flex h-4 touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-44 data-vertical:w-auto data-vertical:flex-col\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{#snippet children({ thumbItems })}\n\t\t<span\n\t\t\tdata-slot=\"audio-player-progress-track\"\n\t\t\tclass=\"bg-muted relative h-[4px] w-full grow overflow-hidden rounded-full\"\n\t\t>\n\t\t\t<SliderPrimitive.Range\n\t\t\t\tdata-slot=\"audio-player-progress-range\"\n\t\t\t\tclass=\"bg-primary absolute h-full\"\n\t\t\t/>\n\t\t</span>\n\t\t{#each thumbItems as thumb (thumb.index)}\n\t\t\t<SliderPrimitive.Thumb\n\t\t\t\tindex={thumb.index}\n\t\t\t\tdata-slot=\"audio-player-progress-thumb\"\n\t\t\t\tclass=\"relative flex h-0 w-0 items-center justify-center opacity-0 group-hover/player:opacity-100 focus-visible:opacity-100 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\"\n\t\t\t>\n\t\t\t\t<div class=\"bg-foreground absolute size-3 rounded-full\"></div>\n\t\t\t</SliderPrimitive.Thumb>\n\t\t{/each}\n\t{/snippet}\n</SliderPrimitive.Root>\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player-progress.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport type { ComponentProps } from \"svelte\";\n\timport Check from \"@lucide/svelte/icons/check\";\n\timport Settings from \"@lucide/svelte/icons/settings\";\n\timport * as DropdownMenu from \"../dropdown-menu/index.js\";\n\timport { Button } from \"../button/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { useAudioPlayer } from \"./context.svelte.js\";\n\n\tconst PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] as const;\n\n\ttype Props = ComponentProps<typeof Button> & {\n\t\tspeeds?: readonly number[];\n\t};\n\n\tlet {\n\t\tspeeds = PLAYBACK_SPEEDS,\n\t\tclass: className,\n\t\tvariant = \"ghost\",\n\t\tsize = \"icon\",\n\t\t...restProps\n\t}: Props = $props();\n\n\tconst player = useAudioPlayer();\n</script>\n\n<DropdownMenu.Root>\n\t<DropdownMenu.Trigger>\n\t\t{#snippet child({ props })}\n\t\t\t<Button\n\t\t\t\t{variant}\n\t\t\t\t{size}\n\t\t\t\tclass={cn(className)}\n\t\t\t\taria-label=\"Playback speed\"\n\t\t\t\t{...props}\n\t\t\t\t{...restProps}\n\t\t\t>\n\t\t\t\t<Settings class=\"size-4\" />\n\t\t\t</Button>\n\t\t{/snippet}\n\t</DropdownMenu.Trigger>\n\t<DropdownMenu.Content align=\"end\" class=\"min-w-[120px]\">\n\t\t{#each speeds as speed (speed)}\n\t\t\t<DropdownMenu.Item\n\t\t\t\tonclick={() => player.setPlaybackRate(speed)}\n\t\t\t\tclass=\"flex items-center justify-between\"\n\t\t\t>\n\t\t\t\t<span class={speed === 1 ? \"\" : \"font-mono\"}>\n\t\t\t\t\t{speed === 1 ? \"Normal\" : `${speed}x`}\n\t\t\t\t</span>\n\t\t\t\t{#if player.playbackRate === speed}\n\t\t\t\t\t<Check class=\"size-4\" />\n\t\t\t\t{/if}\n\t\t\t</DropdownMenu.Item>\n\t\t{/each}\n\t</DropdownMenu.Content>\n</DropdownMenu.Root>\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player-speed.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { Button } from \"../button/index.js\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { useAudioPlayer } from \"./context.svelte.js\";\n\n\ttype Props = Omit<HTMLAttributes<HTMLDivElement>, \"children\"> & {\n\t\tspeeds?: readonly number[];\n\t};\n\n\tlet { speeds = [0.5, 1, 1.5, 2], class: className, ...restProps }: Props = $props();\n\n\tconst player = useAudioPlayer();\n</script>\n\n<div\n\tdata-slot=\"audio-player-speed-button-group\"\n\tclass={cn(\"flex items-center gap-1\", className)}\n\trole=\"group\"\n\taria-label=\"Playback speed controls\"\n\t{...restProps}\n>\n\t{#each speeds as speed (speed)}\n\t\t<Button\n\t\t\tvariant={player.playbackRate === speed ? \"default\" : \"outline\"}\n\t\t\tsize=\"sm\"\n\t\t\tonclick={() => player.setPlaybackRate(speed)}\n\t\t\tclass=\"min-w-[50px] font-mono text-xs\"\n\t\t>\n\t\t\t{speed}x\n\t\t</Button>\n\t{/each}\n</div>\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player-speed-button-group.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn } from \"$UTILS$.js\";\n\timport { useAudioPlayer } from \"./context.svelte.js\";\n\timport { formatTime } from \"./utils.js\";\n\n\tlet { class: className, ...restProps }: HTMLAttributes<HTMLSpanElement> = $props();\n\n\tconst player = useAudioPlayer();\n</script>\n\n<span\n\tdata-slot=\"audio-player-time\"\n\tclass={cn(\"text-muted-foreground text-sm tabular-nums\", className)}\n\t{...restProps}\n>\n\t{formatTime(player.time)}\n</span>\n",
			"type": "registry:ui",
			"target": "audio-player/audio-player-time.svelte"
		},
		{
			"content": "import { getContext, setContext } from \"svelte\";\n\nconst AUDIO_PLAYER_CONTEXT_KEY = Symbol(\"sv11-audio-player\");\n\nexport interface AudioPlayerItem<TData = unknown> {\n\tid: string | number;\n\tsrc: string;\n\tdata?: TData;\n}\n\nexport class AudioPlayerState<TData = unknown> {\n\taudio: HTMLAudioElement | null = $state(null);\n\tactiveItem: AudioPlayerItem<TData> | null = $state(null);\n\ttime = $state(0);\n\tduration = $state<number | undefined>(undefined);\n\tpaused = $state(true);\n\tplaybackRate = $state(1);\n\treadyState = $state(0);\n\tnetworkState = $state(0);\n\terror: MediaError | null = $state(null);\n\n\tisBuffering = $derived(this.readyState < 3 && this.networkState === 2);\n\tisPlaying = $derived(!this.paused);\n\n\t#playPromise: Promise<void> | null = null;\n\n\tisItemActive = (id: string | number | null): boolean => {\n\t\tif (id === null) return this.activeItem === null;\n\t\treturn this.activeItem?.id === id;\n\t};\n\n\t#swapTrack = (item: AudioPlayerItem<TData> | null): void => {\n\t\tconst audio = this.audio;\n\t\tif (!audio) return;\n\t\tthis.activeItem = item;\n\t\tconst currentRate = audio.playbackRate;\n\t\tif (!audio.paused) audio.pause();\n\t\taudio.currentTime = 0;\n\t\tif (item === null) {\n\t\t\taudio.removeAttribute(\"src\");\n\t\t} else {\n\t\t\taudio.src = item.src;\n\t\t}\n\t\taudio.load();\n\t\taudio.playbackRate = currentRate;\n\t};\n\n\tsetActiveItem = async (item: AudioPlayerItem<TData> | null): Promise<void> => {\n\t\tif (!this.audio) return;\n\t\tif ((item?.id ?? null) === (this.activeItem?.id ?? null)) return;\n\t\tthis.#swapTrack(item);\n\t};\n\n\tplay = async (item?: AudioPlayerItem<TData> | null): Promise<void> => {\n\t\tconst audio = this.audio;\n\t\tif (!audio) return;\n\n\t\tif (this.#playPromise) {\n\t\t\ttry {\n\t\t\t\tawait this.#playPromise;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Play promise error:\", error);\n\t\t\t}\n\t\t}\n\n\t\tif (item === undefined) {\n\t\t\tconst playPromise = audio.play();\n\t\t\tthis.#playPromise = playPromise;\n\t\t\treturn playPromise;\n\t\t}\n\t\tif ((item?.id ?? null) === (this.activeItem?.id ?? null)) {\n\t\t\tconst playPromise = audio.play();\n\t\t\tthis.#playPromise = playPromise;\n\t\t\treturn playPromise;\n\t\t}\n\n\t\tthis.#swapTrack(item);\n\t\tconst playPromise = audio.play();\n\t\tthis.#playPromise = playPromise;\n\t\treturn playPromise;\n\t};\n\n\tpause = async (): Promise<void> => {\n\t\tconst audio = this.audio;\n\t\tif (!audio) return;\n\n\t\tif (this.#playPromise) {\n\t\t\ttry {\n\t\t\t\tawait this.#playPromise;\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t}\n\t\t}\n\n\t\taudio.pause();\n\t\tthis.#playPromise = null;\n\t};\n\n\tseek = (time: number): void => {\n\t\tif (!this.audio) return;\n\t\tthis.audio.currentTime = time;\n\t};\n\n\tsetPlaybackRate = (rate: number): void => {\n\t\tif (!this.audio) return;\n\t\tthis.playbackRate = rate;\n\t\tthis.audio.playbackRate = rate;\n\t};\n}\n\nexport function setAudioPlayer<TData = unknown>(): AudioPlayerState<TData> {\n\tconst state = new AudioPlayerState<TData>();\n\tsetContext(AUDIO_PLAYER_CONTEXT_KEY, state);\n\treturn state;\n}\n\nexport function useAudioPlayer<TData = unknown>(): AudioPlayerState<TData> {\n\tconst ctx = getContext<AudioPlayerState<TData> | undefined>(AUDIO_PLAYER_CONTEXT_KEY);\n\tif (!ctx) {\n\t\tthrow new Error(\"useAudioPlayer cannot be called outside of an <AudioPlayer>\");\n\t}\n\treturn ctx;\n}\n",
			"type": "registry:ui",
			"target": "audio-player/context.svelte.ts"
		},
		{
			"content": "/**\n * Lazy shared Web Audio graph for an audio element. Splits context creation\n * from analyser wiring so non-analyser callers (e.g. a scratch synth) can warm\n * the context from a user gesture without forcing a `createMediaElementSource`\n * call — only one is legal per element lifetime.\n */\nexport class AudioGraph {\n\taudioContext = $state<AudioContext | null>(null);\n\tanalyser = $state<AnalyserNode | null>(null);\n\t#source: MediaElementAudioSourceNode | null = null;\n\n\tensureContext(): AudioContext | null {\n\t\tif (this.audioContext) return this.audioContext;\n\t\ttry {\n\t\t\tconst AC =\n\t\t\t\twindow.AudioContext ||\n\t\t\t\t(window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;\n\t\t\tthis.audioContext = new AC();\n\t\t} catch (err) {\n\t\t\tconsole.error(\"AudioContext creation failed\", err);\n\t\t\treturn null;\n\t\t}\n\t\treturn this.audioContext;\n\t}\n\n\tensureAnalyser(audioEl: HTMLAudioElement): AnalyserNode | null {\n\t\tif (this.analyser) return this.analyser;\n\t\tconst ctx = this.ensureContext();\n\t\tif (!ctx) return null;\n\t\tif (ctx.state === \"suspended\") void ctx.resume().catch(() => {});\n\t\ttry {\n\t\t\tthis.#source = ctx.createMediaElementSource(audioEl);\n\t\t\tconst a = ctx.createAnalyser();\n\t\t\ta.fftSize = 512;\n\t\t\ta.smoothingTimeConstant = 0.7;\n\t\t\tthis.#source.connect(a);\n\t\t\ta.connect(ctx.destination);\n\t\t\tthis.analyser = a;\n\t\t\treturn a;\n\t\t} catch (err) {\n\t\t\tconsole.error(\"analyser wire failed\", err);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tdestroy(): void {\n\t\ttry {\n\t\t\tthis.#source?.disconnect();\n\t\t\tthis.analyser?.disconnect();\n\t\t} catch {\n\t\t\t// nodes may already be gone\n\t\t}\n\t\tvoid this.audioContext?.close().catch(() => {});\n\t\tthis.audioContext = null;\n\t\tthis.analyser = null;\n\t\tthis.#source = null;\n\t}\n}\n",
			"type": "registry:ui",
			"target": "audio-player/audio-graph.svelte.ts"
		},
		{
			"content": "export function formatTime(seconds: number): string {\n\tconst hrs = Math.floor(seconds / 3600);\n\tconst mins = Math.floor((seconds % 3600) / 60);\n\tconst secs = Math.floor(seconds % 60);\n\n\tconst formattedMins = mins < 10 ? `0${mins}` : mins;\n\tconst formattedSecs = secs < 10 ? `0${secs}` : secs;\n\n\treturn hrs > 0 ? `${hrs}:${formattedMins}:${formattedSecs}` : `${mins}:${formattedSecs}`;\n}\n",
			"type": "registry:ui",
			"target": "audio-player/utils.ts"
		},
		{
			"content": "/**\n * Default rate used when callers don't pass one. Constant-rate sampling (as\n * opposed to a fixed total bar count) keeps scroll speed and detail identical\n * across songs of different lengths.\n */\nexport const DEFAULT_BARS_PER_SECOND = 8;\n\n/**\n * Pure sampler: given a mono Float32 PCM channel, returns `bars` normalized\n * amplitude values in `[0, 1]`. The algorithm walks every 100th sample in each\n * bucket to stay cheap and multiplies by 3 so quiet tracks still read visually.\n *\n * Values are quantized to 2 decimal places — bars render at ≤51 physical px at\n * 2× DPR, so finer precision isn't visible and 2dp cuts shipped JSON size ~3×.\n *\n * The same function runs in both the browser (inside `precomputeWaveform`\n * below) and the Vite plugin (`vite/waveforms-plugin.ts`) so shipped JSONs\n * and the runtime fallback agree by construction.\n */\nexport function sampleWaveform(channelData: Float32Array, bars: number): number[] {\n\tconst samplesPerBar = Math.floor(channelData.length / bars);\n\tconst out: number[] = [];\n\tfor (let i = 0; i < bars; i++) {\n\t\tconst start = i * samplesPerBar;\n\t\tconst end = start + samplesPerBar;\n\t\tlet sum = 0;\n\t\tlet count = 0;\n\t\tfor (let j = start; j < end && j < channelData.length; j += 100) {\n\t\t\tsum += Math.abs(channelData[j]);\n\t\t\tcount++;\n\t\t}\n\t\tconst avg = count > 0 ? sum / count : 0;\n\t\tout.push(Math.round(Math.min(1, avg * 3) * 100) / 100);\n\t}\n\treturn out;\n}\n\n/**\n * Browser fallback: fetch an audio URL, decode it via `OfflineAudioContext`,\n * and sample the first channel at `barsPerSecond` amplitude values per second\n * of audio.\n *\n * Prefer shipping precomputed waveform JSONs (via the Vite plugin) and\n * pointing tracks at them with `waveformUrl`. This function is the lazy\n * fallback for tracks without one.\n */\nexport async function precomputeWaveform(\n\turl: string,\n\tbarsPerSecond = DEFAULT_BARS_PER_SECOND\n): Promise<number[]> {\n\tconst response = await fetch(url);\n\tconst arrayBuffer = await response.arrayBuffer();\n\tconst OfflineCtx =\n\t\twindow.OfflineAudioContext ||\n\t\t(window as unknown as { webkitOfflineAudioContext: typeof OfflineAudioContext })\n\t\t\t.webkitOfflineAudioContext;\n\t// Length is irrelevant — we only use the context to invoke decodeAudioData,\n\t// which returns an AudioBuffer sized to the source. Use the minimum valid\n\t// value to make that intent obvious.\n\tconst offlineContext = new OfflineCtx(1, 1, 44100);\n\tconst audioBuffer = await offlineContext.decodeAudioData(arrayBuffer.slice(0));\n\tconst bars = Math.max(1, Math.round(audioBuffer.duration * barsPerSecond));\n\treturn sampleWaveform(audioBuffer.getChannelData(0), bars);\n}\n",
			"type": "registry:ui",
			"target": "audio-player/waveform-sampler.ts"
		},
		{
			"content": "export type ExampleTrack = {\n\tid: string;\n\tname: string;\n\turl: string;\n\t/** Precomputed waveform bars inlined. Highest priority. */\n\twaveform?: number[];\n\t/** URL to a JSON file containing precomputed bars. Second priority. */\n\twaveformUrl?: string;\n};\n\nconst TRACK_NAMES = [\n\t\"alpha\",\n\t\"bravo\",\n\t\"charlie\",\n\t\"delta\",\n\t\"echo\",\n\t\"foxtrot\",\n\t\"golf\",\n\t\"hotel\",\n\t\"india\",\n\t\"juliett\",\n] as const;\n\nexport const exampleTracks: ExampleTrack[] = TRACK_NAMES.map((name, i) => ({\n\tid: String(i),\n\tname: name.charAt(0).toUpperCase() + name.slice(1),\n\turl: `https://sv11.ui.twango.dev/audio/${name}.mp3`,\n\twaveformUrl: `https://sv11.ui.twango.dev/audio/waveforms/${name}.json`,\n}));\n",
			"type": "registry:ui",
			"target": "audio-player/example-tracks.ts"
		},
		{
			"content": "import Root from \"./audio-player.svelte\";\nimport Button from \"./audio-player-button.svelte\";\nimport Progress from \"./audio-player-progress.svelte\";\nimport Time from \"./audio-player-time.svelte\";\nimport Duration from \"./audio-player-duration.svelte\";\nimport Speed from \"./audio-player-speed.svelte\";\nimport SpeedButtonGroup from \"./audio-player-speed-button-group.svelte\";\n\nexport {\n\tRoot,\n\tButton,\n\tProgress,\n\tTime,\n\tDuration,\n\tSpeed,\n\tSpeedButtonGroup,\n\t//\n\tRoot as AudioPlayer,\n\tButton as AudioPlayerButton,\n\tProgress as AudioPlayerProgress,\n\tTime as AudioPlayerTime,\n\tDuration as AudioPlayerDuration,\n\tSpeed as AudioPlayerSpeed,\n\tSpeedButtonGroup as AudioPlayerSpeedButtonGroup,\n};\n\nexport { setAudioPlayer, useAudioPlayer, AudioPlayerState } from \"./context.svelte.js\";\nexport type { AudioPlayerItem } from \"./context.svelte.js\";\nexport { formatTime } from \"./utils.js\";\nexport { exampleTracks } from \"./example-tracks.js\";\nexport { precomputeWaveform, sampleWaveform } from \"./waveform-sampler.js\";\nexport { AudioGraph } from \"./audio-graph.svelte.js\";\n",
			"type": "registry:ui",
			"target": "audio-player/index.ts"
		}
	]
}