Audio Player
Audio player on media-chrome: play, seek, time, mute and volume, styled as an Edmi card.
import { AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay, AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange,} from "@/components/ai/audio-player";import { useEffect, useState } from "react";
// Demo audio: a short synthesized clip so the page works offline. Use any URL, or `data` from the AI SDK.function makeClip(seconds: number) { const rate = 8000; const n = rate * seconds; const buf = new ArrayBuffer(44 + n); const v = new DataView(buf); const text = (o: number, s: string) => { for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i)); }; text(0, "RIFF"); v.setUint32(4, 36 + n, true); text(8, "WAVEfmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true); v.setUint32(24, rate, true); v.setUint32(28, rate, true); v.setUint16(32, 1, true); v.setUint16(34, 8, true); text(36, "data"); v.setUint32(40, n, true); for (let i = 0; i < n; i++) { const t = i / rate; const gate = Math.sin(Math.PI * 2 * t * 1.5) > 0 ? 1 : 0.35; v.setUint8( 44 + i, 128 + 14 * gate * Math.sin(2 * Math.PI * (200 + 30 * Math.floor(t)) * t), ); } return URL.createObjectURL(new Blob([buf], { type: "audio/wav" }));}
export default function Demo() { const [src, setSrc] = useState<string>(); useEffect(() => { const url = makeClip(42); setSrc(url); return () => URL.revokeObjectURL(url); }, []);
return ( <div className="flex w-full max-w-xl flex-col gap-4"> <AudioPlayer> {src && <AudioPlayerElement src={src} />} <AudioPlayerControlBar> <AudioPlayerSeekBackwardButton seekOffset={10} /> <AudioPlayerPlayButton /> <AudioPlayerSeekForwardButton seekOffset={10} /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> <AudioPlayerMuteButton /> <AudioPlayerVolumeRange /> </AudioPlayerControlBar> </AudioPlayer> <AudioPlayer className="max-w-sm"> {src && <AudioPlayerElement src={src} />} <AudioPlayerControlBar> <AudioPlayerPlayButton /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> </AudioPlayerControlBar> </AudioPlayer> </div> );}<script setup lang="ts">import { AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay, AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange,} from "@/components/ai/audio-player";import { onBeforeUnmount, onMounted, ref } from "vue";
// Demo audio: a short synthesized clip so the page works offline. Use any URL, or `data` from the AI SDK.function makeClip(seconds: number) { const rate = 8000; const n = rate * seconds; const buf = new ArrayBuffer(44 + n); const v = new DataView(buf); const text = (o: number, s: string) => { for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i)); }; text(0, "RIFF"); v.setUint32(4, 36 + n, true); text(8, "WAVEfmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true); v.setUint32(24, rate, true); v.setUint32(28, rate, true); v.setUint16(32, 1, true); v.setUint16(34, 8, true); text(36, "data"); v.setUint32(40, n, true); for (let i = 0; i < n; i++) { const t = i / rate; const gate = Math.sin(Math.PI * 2 * t * 1.5) > 0 ? 1 : 0.35; v.setUint8(44 + i, 128 + 14 * gate * Math.sin(2 * Math.PI * (200 + 30 * Math.floor(t)) * t)); } return URL.createObjectURL(new Blob([buf], { type: "audio/wav" }));}
const src = ref<string>();onMounted(() => { src.value = makeClip(42);});onBeforeUnmount(() => { if (src.value) URL.revokeObjectURL(src.value);});</script>
<template> <div class="flex w-full max-w-xl flex-col gap-4"> <AudioPlayer> <AudioPlayerElement v-if="src" :src="src" /> <AudioPlayerControlBar> <AudioPlayerSeekBackwardButton :seek-offset="10" /> <AudioPlayerPlayButton /> <AudioPlayerSeekForwardButton :seek-offset="10" /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> <AudioPlayerMuteButton /> <AudioPlayerVolumeRange /> </AudioPlayerControlBar> </AudioPlayer> <AudioPlayer class="max-w-sm"> <AudioPlayerElement v-if="src" :src="src" /> <AudioPlayerControlBar> <AudioPlayerPlayButton /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> </AudioPlayerControlBar> </AudioPlayer> </div></template><script lang="ts"> import { AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay, AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange, } from "$lib/components/ai/audio-player"; import { onMount } from "svelte";
// Demo audio: a short synthesized clip so the page works offline. Use any URL, or `data` from the AI SDK. function makeClip(seconds: number) { const rate = 8000; const n = rate * seconds; const buf = new ArrayBuffer(44 + n); const v = new DataView(buf); const text = (o: number, s: string) => { for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i)); }; text(0, "RIFF"); v.setUint32(4, 36 + n, true); text(8, "WAVEfmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true); v.setUint32(24, rate, true); v.setUint32(28, rate, true); v.setUint16(32, 1, true); v.setUint16(34, 8, true); text(36, "data"); v.setUint32(40, n, true); for (let i = 0; i < n; i++) { const t = i / rate; const gate = Math.sin(Math.PI * 2 * t * 1.5) > 0 ? 1 : 0.35; v.setUint8(44 + i, 128 + 14 * gate * Math.sin(2 * Math.PI * (200 + 30 * Math.floor(t)) * t)); } return URL.createObjectURL(new Blob([buf], { type: "audio/wav" })); }
let src = $state<string>(); onMount(() => { const url = makeClip(42); src = url; return () => URL.revokeObjectURL(url); });</script>
<div class="flex w-full max-w-xl flex-col gap-4"> <AudioPlayer> {#if src}<AudioPlayerElement {src} />{/if} <AudioPlayerControlBar> <AudioPlayerSeekBackwardButton seekOffset={10} /> <AudioPlayerPlayButton /> <AudioPlayerSeekForwardButton seekOffset={10} /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> <AudioPlayerMuteButton /> <AudioPlayerVolumeRange /> </AudioPlayerControlBar> </AudioPlayer> <AudioPlayer class="max-w-sm"> {#if src}<AudioPlayerElement {src} />{/if} <AudioPlayerControlBar> <AudioPlayerPlayButton /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> </AudioPlayerControlBar> </AudioPlayer></div>Built on button; the ranges are media-chrome ranges themed through its CSS variables.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-audio-playerpnpm dlx shadcn@latest add @edmi-ui/ai-audio-playeryarn dlx shadcn@latest add @edmi-ui/ai-audio-playerbunx shadcn@latest add @edmi-ui/ai-audio-playerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install ai media-chromeTerminal window pnpm add ai media-chromeTerminal window yarn add ai media-chromeTerminal window bun add ai media-chromeAdd the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-audio-playerpnpm dlx shadcn-vue@latest add @edmi-ui/ai-audio-playeryarn dlx shadcn-vue@latest add @edmi-ui/ai-audio-playerbunx shadcn-vue@latest add @edmi-ui/ai-audio-playerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install media-chrome aiTerminal window pnpm add media-chrome aiTerminal window yarn add media-chrome aiTerminal window bun add media-chrome aiAdd the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install media-chrome@^4.19.3Terminal window pnpm add media-chrome@^4.19.3Terminal window yarn add media-chrome@^4.19.3Terminal window bun add media-chrome@^4.19.3Add the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
import { AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay, AudioPlayerMuteButton, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange,} from "@/components/ai/audio-player";
<div className="flex w-full max-w-xl flex-col gap-4"> <AudioPlayer> <AudioPlayerControlBar> <AudioPlayerSeekBackwardButton seekOffset={10} /> <AudioPlayerPlayButton /> <AudioPlayerSeekForwardButton seekOffset={10} /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> <AudioPlayerMuteButton /> <AudioPlayerVolumeRange /> </AudioPlayerControlBar> </AudioPlayer> <AudioPlayer className="max-w-sm"> <AudioPlayerControlBar> <AudioPlayerPlayButton /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> </AudioPlayerControlBar> </AudioPlayer></div><script setup lang="ts">import { AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay, AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange,} from "@/components/ai/audio-player";</script>
<template> <div class="flex w-full max-w-xl flex-col gap-4"> <AudioPlayer> <AudioPlayerElement /> <AudioPlayerControlBar> <AudioPlayerSeekBackwardButton :seek-offset="10" /> <AudioPlayerPlayButton /> <AudioPlayerSeekForwardButton :seek-offset="10" /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> <AudioPlayerMuteButton /> <AudioPlayerVolumeRange /> </AudioPlayerControlBar> </AudioPlayer> <AudioPlayer class="max-w-sm"> <AudioPlayerElement /> <AudioPlayerControlBar> <AudioPlayerPlayButton /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> </AudioPlayerControlBar> </AudioPlayer> </div></template><script lang="ts"> import { AudioPlayer, AudioPlayerControlBar, AudioPlayerDurationDisplay, AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerVolumeRange, } from "$lib/components/ai/audio-player";</script>
<div class="flex w-full max-w-xl flex-col gap-4"> <AudioPlayer> {#if src}<AudioPlayerElement />{/if} <AudioPlayerControlBar> <AudioPlayerSeekBackwardButton seekOffset={10} /> <AudioPlayerPlayButton /> <AudioPlayerSeekForwardButton seekOffset={10} /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> <AudioPlayerMuteButton /> <AudioPlayerVolumeRange /> </AudioPlayerControlBar> </AudioPlayer> <AudioPlayer class="max-w-sm"> {#if src}<AudioPlayerElement />{/if} <AudioPlayerControlBar> <AudioPlayerPlayButton /> <AudioPlayerTimeDisplay /> <AudioPlayerTimeRange /> <AudioPlayerDurationDisplay /> </AudioPlayerControlBar> </AudioPlayer></div>Component source
Section titled “Component source”"use client";
import type { Experimental_SpeechResult as SpeechResult } from "ai";import { cn } from "cn";import { MediaControlBar, MediaController, MediaDurationDisplay, MediaMuteButton, MediaPlayButton, MediaSeekBackwardButton, MediaSeekForwardButton, MediaTimeDisplay, MediaTimeRange, MediaVolumeRange,} from "media-chrome/react";import type { ComponentProps, CSSProperties } from "react";import { buttonVariants } from "@/registry/edmi/ui/button";
// Built on ui/button (DESIGN §5b): round primary play button, ghost icon buttons for seek and mute, mono// tabular times. The ranges are media-chrome ranges themed through its CSS variables (brand fill, --muted track).
export type AudioPlayerProps = Omit< ComponentProps<typeof MediaController>, "audio">;
/** Card shell (`--card`, 1px border) around the media-chrome controller; the controls are children. */export const AudioPlayer = ({ children, className, style, ...props}: AudioPlayerProps) => ( <MediaController audio data-slot="ai-audio-player" className={cn( "block w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-foreground", className, )} style={ { "--media-background-color": "transparent", "--media-button-icon-height": "0.875rem", "--media-button-icon-width": "0.875rem", "--media-control-background": "transparent", "--media-control-hover-background": "transparent", "--media-control-padding": "0", "--media-font": "var(--font-sans)", "--media-font-size": "12px", "--media-icon-color": "currentColor", "--media-preview-time-background": "var(--popover)", "--media-preview-time-border-radius": "6px", "--media-preview-time-text-shadow": "none", "--media-primary-color": "var(--foreground)", "--media-range-bar-color": "var(--brand)", "--media-range-track-background": "var(--border)", "--media-range-track-border-radius": "999px", "--media-range-track-height": "4px", "--media-range-thumb-background": "var(--brand)", "--media-range-thumb-border-radius": "999px", "--media-range-thumb-height": "12px", "--media-range-thumb-width": "12px", "--media-secondary-color": "var(--muted)", "--media-text-color": "var(--foreground)", "--media-tooltip-arrow-display": "none", "--media-tooltip-background": "var(--popover)", "--media-tooltip-border-radius": "6px", ...style, } as CSSProperties } {...props} > {children} </MediaController>);
export type AudioPlayerElementProps = Omit<ComponentProps<"audio">, "src"> & ( | { data: SpeechResult["audio"]; } | { src: string; } );
export const AudioPlayerElement = ({ ...props }: AudioPlayerElementProps) => ( // Captions are provided by the consumer. <audio data-slot="ai-audio-player-element" slot="media" src={ "src" in props ? props.src : `data:${props.data.mediaType};base64,${props.data.base64}` } {...props} />);
export type AudioPlayerControlBarProps = ComponentProps<typeof MediaControlBar>;
export const AudioPlayerControlBar = ({ className, ...props}: AudioPlayerControlBarProps) => ( <MediaControlBar data-slot="ai-audio-player-control-bar" className={cn("flex w-full items-center gap-2", className)} {...props} />);
export type AudioPlayerPlayButtonProps = ComponentProps<typeof MediaPlayButton>;
/** The only filled control: a 36px round primary button. */export const AudioPlayerPlayButton = ({ className, ...props}: AudioPlayerPlayButtonProps) => ( <MediaPlayButton data-slot="ai-audio-player-play-button" className={cn( buttonVariants({ variant: "default", size: "icon" }), "rounded-full", className, )} {...props} />);
export type AudioPlayerSeekBackwardButtonProps = ComponentProps< typeof MediaSeekBackwardButton>;
export const AudioPlayerSeekBackwardButton = ({ seekOffset = 10, className, ...props}: AudioPlayerSeekBackwardButtonProps) => ( <MediaSeekBackwardButton data-slot="ai-audio-player-seek-backward-button" className={cn( buttonVariants({ variant: "ghost", size: "icon-sm" }), className, )} seekOffset={seekOffset} {...props} />);
export type AudioPlayerSeekForwardButtonProps = ComponentProps< typeof MediaSeekForwardButton>;
export const AudioPlayerSeekForwardButton = ({ seekOffset = 10, className, ...props}: AudioPlayerSeekForwardButtonProps) => ( <MediaSeekForwardButton data-slot="ai-audio-player-seek-forward-button" className={cn( buttonVariants({ variant: "ghost", size: "icon-sm" }), className, )} seekOffset={seekOffset} {...props} />);
export type AudioPlayerTimeDisplayProps = ComponentProps< typeof MediaTimeDisplay>;
export const AudioPlayerTimeDisplay = ({ className, ...props}: AudioPlayerTimeDisplayProps) => ( <MediaTimeDisplay data-slot="ai-audio-player-time-display" className={cn( "font-mono text-xs text-foreground tabular-nums [--media-font:var(--font-mono)]", className, )} {...props} />);
export type AudioPlayerTimeRangeProps = ComponentProps<typeof MediaTimeRange>;
export const AudioPlayerTimeRange = ({ className, ...props}: AudioPlayerTimeRangeProps) => ( <MediaTimeRange data-slot="ai-audio-player-time-range" className={cn("h-7 min-w-0 flex-1 px-1", className)} {...props} />);
export type AudioPlayerDurationDisplayProps = ComponentProps< typeof MediaDurationDisplay>;
export const AudioPlayerDurationDisplay = ({ className, ...props}: AudioPlayerDurationDisplayProps) => ( <MediaDurationDisplay data-slot="ai-audio-player-duration-display" className={cn( "font-mono text-xs text-muted-foreground tabular-nums [--media-font:var(--font-mono)] [--media-text-color:var(--muted-foreground)]", className, )} {...props} />);
export type AudioPlayerMuteButtonProps = ComponentProps<typeof MediaMuteButton>;
export const AudioPlayerMuteButton = ({ className, ...props}: AudioPlayerMuteButtonProps) => ( <MediaMuteButton data-slot="ai-audio-player-mute-button" className={cn( buttonVariants({ variant: "ghost", size: "icon-sm" }), className, )} {...props} />);
export type AudioPlayerVolumeRangeProps = ComponentProps< typeof MediaVolumeRange>;
export const AudioPlayerVolumeRange = ({ className, ...props}: AudioPlayerVolumeRangeProps) => ( <MediaVolumeRange data-slot="ai-audio-player-volume-range" className={cn("h-7 w-[70px] px-1", className)} {...props} />);<script setup lang="ts">import type { HTMLAttributes, StyleValue } from "vue"import "media-chrome"import { cn } from "@/registry/edmi/lib/utils"
// Card shell (`--card`, 1px border) around the media-chrome controller; the controls are the default slot.// Built on ui/button for the buttons; media-chrome ranges are themed through its CSS variables.const props = defineProps<{ class?: HTMLAttributes["class"] style?: StyleValue}>()
const mediaStyle = { "--media-background-color": "transparent", "--media-button-icon-height": "0.875rem", "--media-button-icon-width": "0.875rem", "--media-control-background": "transparent", "--media-control-hover-background": "transparent", "--media-control-padding": "0", "--media-font": "var(--font-sans)", "--media-font-size": "12px", "--media-icon-color": "currentColor", "--media-preview-time-background": "var(--popover)", "--media-preview-time-border-radius": "6px", "--media-preview-time-text-shadow": "none", "--media-primary-color": "var(--foreground)", "--media-range-bar-color": "var(--brand)", "--media-range-track-background": "var(--border)", "--media-range-track-border-radius": "999px", "--media-range-track-height": "4px", "--media-range-thumb-background": "var(--brand)", "--media-range-thumb-border-radius": "999px", "--media-range-thumb-height": "12px", "--media-range-thumb-width": "12px", "--media-secondary-color": "var(--muted)", "--media-text-color": "var(--foreground)", "--media-tooltip-arrow-display": "none", "--media-tooltip-background": "var(--popover)", "--media-tooltip-border-radius": "6px", }</script>
<template> <component :is="'media-controller'" audio data-slot="ai-audio-player" :class="cn('block w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-foreground', props.class)" :style="[mediaStyle, props.style]" > <slot /> </component></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()</script>
<template> <component :is="'media-control-bar'" data-slot="ai-audio-player-control-bar" :class="cn('flex w-full items-center gap-2', props.class)" > <slot /> </component></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()</script>
<template> <component :is="'media-duration-display'" data-slot="ai-audio-player-duration-display" :class="cn('font-mono text-xs text-muted-foreground tabular-nums [--media-font:var(--font-mono)] [--media-text-color:var(--muted-foreground)]', props.class)" /></template><script setup lang="ts">import type { Experimental_SpeechResult as SpeechResult } from "ai"import { computed } from "vue"
defineOptions({ inheritAttrs: false })
const props = defineProps<{ src?: string data?: SpeechResult["audio"]}>()
const audioSrc = computed(() => { if (props.src) return props.src if (props.data) return `data:${props.data.mediaType};base64,${props.data.base64}` return undefined})</script>
<template> <!-- Captions are provided by the consumer. --> <audio v-bind="{ ...$attrs, slot: 'media' }" data-slot="ai-audio-player-element" :src="audioSrc" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] }>(), {})</script>
<template> <component :is="'media-mute-button'" data-slot="ai-audio-player-mute-button" :class="cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] }>(), {})</script>
<template> <component :is="'media-play-button'" data-slot="ai-audio-player-play-button" :class="cn(buttonVariants({ variant: 'default', size: 'icon' }), 'rounded-full', props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"], seekOffset?: number }>(), {seekOffset: 10})</script>
<template> <component :is="'media-seek-backward-button'" data-slot="ai-audio-player-seek-backward-button" :seekoffset="props.seekOffset" :class="cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"], seekOffset?: number }>(), {seekOffset: 10})</script>
<template> <component :is="'media-seek-forward-button'" data-slot="ai-audio-player-seek-forward-button" :seekoffset="props.seekOffset" :class="cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()</script>
<template> <component :is="'media-time-display'" data-slot="ai-audio-player-time-display" :class="cn('font-mono text-xs text-foreground tabular-nums [--media-font:var(--font-mono)]', props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()</script>
<template> <component :is="'media-time-range'" data-slot="ai-audio-player-time-range" :class="cn('h-7 min-w-0 flex-1 px-1', props.class)" /></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()</script>
<template> <component :is="'media-volume-range'" data-slot="ai-audio-player-volume-range" :class="cn('h-7 w-[70px] px-1', props.class)" /></template>export { default as AudioPlayer } from "./AudioPlayer.vue";export { default as AudioPlayerControlBar } from "./AudioPlayerControlBar.vue";export { default as AudioPlayerDurationDisplay } from "./AudioPlayerDurationDisplay.vue";export { default as AudioPlayerElement } from "./AudioPlayerElement.vue";export { default as AudioPlayerMuteButton } from "./AudioPlayerMuteButton.vue";export { default as AudioPlayerPlayButton } from "./AudioPlayerPlayButton.vue";export { default as AudioPlayerSeekBackwardButton } from "./AudioPlayerSeekBackwardButton.vue";export { default as AudioPlayerSeekForwardButton } from "./AudioPlayerSeekForwardButton.vue";export { default as AudioPlayerTimeDisplay } from "./AudioPlayerTimeDisplay.vue";export { default as AudioPlayerTimeRange } from "./AudioPlayerTimeRange.vue";export { default as AudioPlayerVolumeRange } from "./AudioPlayerVolumeRange.vue";<script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-control-bar data-slot="ai-audio-player-control-bar" class={cn("flex w-full items-center gap-2", className)} {...restProps}> {@render children?.()}</media-control-bar><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-duration-display data-slot="ai-audio-player-duration-display" class={cn("font-mono text-xs text-muted-foreground tabular-nums [--media-font:var(--font-mono)] [--media-text-color:var(--muted-foreground)]", className)} {...restProps}> {@render children?.()}</media-duration-display><script lang="ts"> import type { Experimental_SpeechResult as SpeechResult } from "ai"; import type { HTMLAudioAttributes } from "svelte/elements";
// Pass `src`, or `data` (an AI SDK speech result). Captions are provided by the consumer. let { src, data, ...restProps }: Omit<HTMLAudioAttributes, "src"> & { src?: string; data?: SpeechResult["audio"]; } = $props();
const audioSrc = $derived( src ?? (data ? `data:${data.mediaType};base64,${data.base64}` : undefined) );</script>
<!-- svelte-ignore a11y_media_has_caption --><audio data-slot="ai-audio-player-element" src={audioSrc} {...restProps} {...{ slot: "media" }}></audio><script lang="ts"> import { buttonVariants } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-mute-button data-slot="ai-audio-player-mute-button" class={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), className)} {...restProps}> {@render children?.()}</media-mute-button><script lang="ts"> import { buttonVariants } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-play-button data-slot="ai-audio-player-play-button" class={cn(buttonVariants({ variant: "default", size: "icon" }), "rounded-full", className)} {...restProps}> {@render children?.()}</media-play-button><script lang="ts"> import { buttonVariants } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, seekOffset = 10, children, ...restProps }: HTMLAttributes<HTMLElement> & { seekOffset?: number } = $props();</script>
<media-seek-backward-button data-slot="ai-audio-player-seek-backward-button" seekoffset={seekOffset} class={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), className)} {...restProps}> {@render children?.()}</media-seek-backward-button><script lang="ts"> import { buttonVariants } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, seekOffset = 10, children, ...restProps }: HTMLAttributes<HTMLElement> & { seekOffset?: number } = $props();</script>
<media-seek-forward-button data-slot="ai-audio-player-seek-forward-button" seekoffset={seekOffset} class={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), className)} {...restProps}> {@render children?.()}</media-seek-forward-button><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-time-display data-slot="ai-audio-player-time-display" class={cn("font-mono text-xs text-foreground tabular-nums [--media-font:var(--font-mono)]", className)} {...restProps}> {@render children?.()}</media-time-display><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-time-range data-slot="ai-audio-player-time-range" class={cn("h-7 min-w-0 flex-1 px-1", className)} {...restProps}> {@render children?.()}</media-time-range><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();</script>
<media-volume-range data-slot="ai-audio-player-volume-range" class={cn("h-7 w-[70px] px-1", className)} {...restProps}> {@render children?.()}</media-volume-range><script lang="ts"> import { cn } from "$lib/utils.js"; import { onMount } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
// Card shell (`--card`, 1px border) around the media-chrome controller; the controls are children. // Built on ui/button for the buttons; media-chrome ranges are themed through its CSS variables. let { class: className, style, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
// Registers the <media-*> custom elements in the browser only (SSR-safe). onMount(() => { import("media-chrome"); });
const mediaStyle = "--media-background-color: transparent; --media-button-icon-height: 0.875rem; --media-button-icon-width: 0.875rem; --media-control-background: transparent; --media-control-hover-background: transparent; --media-control-padding: 0; --media-font: var(--font-sans); --media-font-size: 12px; --media-icon-color: currentColor; --media-preview-time-background: var(--popover); --media-preview-time-border-radius: 6px; --media-preview-time-text-shadow: none; --media-primary-color: var(--foreground); --media-range-bar-color: var(--brand); --media-range-track-background: var(--border); --media-range-track-border-radius: 999px; --media-range-track-height: 4px; --media-range-thumb-background: var(--brand); --media-range-thumb-border-radius: 999px; --media-range-thumb-height: 12px; --media-range-thumb-width: 12px; --media-secondary-color: var(--muted); --media-text-color: var(--foreground); --media-tooltip-arrow-display: none; --media-tooltip-background: var(--popover); --media-tooltip-border-radius: 6px;";</script>
<media-controller audio data-slot="ai-audio-player" class={cn( "block w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-foreground", className )} style="{mediaStyle} {style ?? ''}" {...restProps}> {@render children?.()}</media-controller>import Root from "./audio-player.svelte";import ControlBar from "./audio-player-control-bar.svelte";import DurationDisplay from "./audio-player-duration-display.svelte";import Element from "./audio-player-element.svelte";import MuteButton from "./audio-player-mute-button.svelte";import PlayButton from "./audio-player-play-button.svelte";import SeekBackwardButton from "./audio-player-seek-backward-button.svelte";import SeekForwardButton from "./audio-player-seek-forward-button.svelte";import TimeDisplay from "./audio-player-time-display.svelte";import TimeRange from "./audio-player-time-range.svelte";import VolumeRange from "./audio-player-volume-range.svelte";
export { ControlBar, ControlBar as AudioPlayerControlBar, DurationDisplay, DurationDisplay as AudioPlayerDurationDisplay, Element, Element as AudioPlayerElement, MuteButton, MuteButton as AudioPlayerMuteButton, PlayButton, PlayButton as AudioPlayerPlayButton, Root, // Root as AudioPlayer, SeekBackwardButton, SeekBackwardButton as AudioPlayerSeekBackwardButton, SeekForwardButton, SeekForwardButton as AudioPlayerSeekForwardButton, TimeDisplay, TimeDisplay as AudioPlayerTimeDisplay, TimeRange, TimeRange as AudioPlayerTimeRange, VolumeRange, VolumeRange as AudioPlayerVolumeRange,};| Part | Prop | Type | Default |
|---|---|---|---|
| AudioPlayer | className, style |
merged into the media-chrome controller (--media-* variables can be overridden in style) |
|
| AudioPlayerElement | src or data |
string, or an AI SDK speech audio result |
|
| AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton | seekOffset |
number (seconds) |
10 |
| other parts | media-chrome element props |
✦ Edmi additions
Section titled “✦ Edmi additions”- The player is a
--cardsurface with a 1px border; play is a 36px round primary button, seek and mute are ghost icon buttons. - Times are mono and tabular; the time range fills with
--brandover a--mutedtrack. - Deviation from the board: the board draws a waveform for the time range; the real component uses the media-chrome range (a waveform needs decoded audio).
Framework notes
Section titled “Framework notes”- Vue and Svelte use the
<media-*>custom elements directly (seek-offset/seekOffsetprop on the seek buttons). In Svelte the root registers media-chrome on mount.