{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-player",
  "title": "Audio Player",
  "description": "Composable transport, seeking, time, volume, rate and loop controls.",
  "dependencies": [
    "solid-js@^1.9.15",
    "@kobalte/core@https://github.com/jhomra21/kobalte/releases/download/kobalte-solid1-audiocn-e0e3bf095f05c7e61230a251b79181c6d834d408/kobalte-core-0.13.14-audiocn.0.e0e3bf09.tgz"
  ],
  "registryDependencies": [
    "@audiocn-solid/core",
    "@audiocn-solid/volume-control",
    "@audiocn-solid/use-audio-player",
    "@audiocn-solid/use-frame-source"
  ],
  "files": [
    {
      "path": "components/ui/audio-player.tsx",
      "content": "import * as SliderPrimitive from \"@kobalte/core/slider\";\nimport {\n  Show,\n  createContext,\n  createMemo,\n  createSignal,\n  untrack,\n  useContext,\n} from \"solid-js\";\n\nimport {\n  VolumeControl,\n  VolumeControlMute,\n  VolumeControlSlider,\n} from \"@/components/ui/volume-control\";\nimport type { VolumeControlProps } from \"@/components/ui/volume-control\";\nimport { useAudioPlayer } from \"@/hooks/use-audio-player\";\nimport type {\n  AudioPlayerController,\n  UseAudioPlayerOptions,\n} from \"@/hooks/use-audio-player\";\nimport { useFrameSource } from \"@/hooks/use-frame-source\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport { formatTime } from \"@/lib/audio/time\";\nimport { provideContext } from \"@/lib/solid/context\";\nimport { createCompatEffect } from \"@/lib/solid/effect\";\nimport type {\n  ButtonDOMProps,\n  DivDOMProps,\n  ImageDOMProps,\n  JSXElement,\n  SpanDOMProps,\n} from \"@/lib/solid/jsx-types\";\nimport { useKobalteSliderCompat } from \"@/lib/solid/kobalte-slider\";\nimport { forwardProps, omitProps } from \"@/lib/solid/props\";\nimport { mergeStyleVars } from \"@/lib/solid/style\";\nimport type { StyleValue } from \"@/lib/solid/style\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];\n\nconst SEEK_STEP = 5;\n\nconst SEEK_LARGE_STEP = 15;\n\nconst VOLUME_STEP = 0.05;\n\ninterface PlayerContextValue {\n  readonly player: AudioPlayerController;\n  readonly onPrevious?: () => void;\n  readonly onNext?: () => void;\n}\n\nconst PlayerContext = createContext<PlayerContextValue>();\n\nconst usePlayerPart = (part: string) => {\n  const context = useContext(PlayerContext);\n\n  if (!context) throw new Error(`${part} must be used inside AudioPlayer.`);\n\n  return context;\n};\n\nexport const useAudioPlayerContext = () =>\n  usePlayerPart(\"useAudioPlayerContext\").player;\n\nexport interface AudioPlayerProps\n  extends\n    Omit<\n      DivDOMProps,\n      | \"onPlay\"\n      | \"onPause\"\n      | \"onEnded\"\n      | \"onError\"\n      | \"onTimeUpdate\"\n      | \"onKeyDown\"\n    >,\n    UseAudioPlayerOptions {\n  className?: string;\n  player?: AudioPlayerController;\n  onTimeUpdate?: (time: number) => void;\n  onPrevious?: () => void;\n  onNext?: () => void;\n  shortcuts?: boolean;\n  onKeyDown?: (event: KeyboardEvent) => void;\n}\n\nconst isEditable = (target: EventTarget | null) =>\n  target instanceof HTMLElement &&\n  (target.isContentEditable ||\n    [\"INPUT\", \"SELECT\", \"TEXTAREA\"].includes(target.tagName));\n\nexport const AudioPlayer = (props: AudioPlayerProps) => {\n  return (\n    <Show\n      when={props.player}\n      keyed\n      fallback={<InternalAudioPlayer settings={props} />}\n    >\n      {(player) => <AudioPlayerRoot {...props} player={player} />}\n    </Show>\n  );\n};\n\nconst InternalAudioPlayer = (props: { settings: AudioPlayerProps }) => {\n  const player = useAudioPlayer(() => props.settings);\n\n  return <AudioPlayerRoot {...props.settings} player={player} />;\n};\n\nconst AudioPlayerRoot = (\n  props: AudioPlayerProps & { player: AudioPlayerController }\n) => {\n  const player = () => props.player;\n\n  const settings: PlayerContextValue = {\n    get player() {\n      return player();\n    },\n    get onPrevious() {\n      return props.onPrevious;\n    },\n    get onNext() {\n      return props.onNext;\n    },\n  };\n\n  createCompatEffect(\n    () => player().currentTime,\n    (time) => {\n      props.onTimeUpdate?.(time);\n    }\n  );\n\n  const keyDown = (event: KeyboardEvent) => {\n    props.onKeyDown?.(event);\n\n    if (\n      props.shortcuts === false ||\n      event.defaultPrevented ||\n      event.metaKey ||\n      event.ctrlKey ||\n      event.altKey ||\n      isEditable(event.target)\n    )\n      return;\n    const target = event.target;\n\n    if (!(target instanceof HTMLElement)) return;\n    const controller = player();\n    const slider = target.getAttribute(\"role\") === \"slider\";\n    const amount = event.shiftKey ? SEEK_LARGE_STEP : SEEK_STEP;\n\n    const actions = {\n      \" \": target.tagName === \"BUTTON\" ? null : () => void controller.toggle(),\n      k: target.tagName === \"BUTTON\" ? null : () => void controller.toggle(),\n      m: () => controller.setMuted(!controller.muted),\n      ArrowDown: slider\n        ? null\n        : () =>\n            controller.setVolume(clamp(controller.volume - VOLUME_STEP, 0, 1)),\n      ArrowUp: slider\n        ? null\n        : () =>\n            controller.setVolume(clamp(controller.volume + VOLUME_STEP, 0, 1)),\n      ArrowLeft: slider\n        ? null\n        : () => controller.seek(controller.currentTime - amount),\n      ArrowRight: slider\n        ? null\n        : () => controller.seek(controller.currentTime + amount),\n      Home: slider ? null : () => controller.seek(0),\n      End: slider ? null : () => controller.seek(controller.duration),\n    };\n\n    if (!Object.hasOwn(actions, event.key)) return;\n    // SAFETY: The own-property check proves this is a player shortcut.\n    const action = actions[event.key as keyof typeof actions];\n\n    if (action) {\n      event.preventDefault();\n      action();\n    }\n  };\n\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"children\",\n    \"player\",\n    \"onPrevious\",\n    \"onNext\",\n    \"onTimeUpdate\",\n    \"shortcuts\",\n    \"onKeyDown\",\n    \"src\",\n    \"autoPlay\",\n    \"loop\",\n    \"volume\",\n    \"muted\",\n    \"playbackRate\",\n    \"preload\",\n    \"crossOrigin\",\n    \"onPlay\",\n    \"onPause\",\n    \"onEnded\",\n    \"onError\",\n  ]);\n\n  return provideContext(PlayerContext, settings, () => (\n    <div\n      class={cn(\n        \"group/audio-player flex flex-wrap items-center gap-x-3 gap-y-2 outline-none\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-player\"\n      role=\"group\"\n      data-ended={player().status === \"ended\" ? \"\" : undefined}\n      data-error={player().status === \"error\" ? \"\" : undefined}\n      data-loading={player().status === \"loading\" ? \"\" : undefined}\n      data-muted={player().muted ? \"\" : undefined}\n      data-paused={player().playing ? undefined : \"\"}\n      data-playing={player().playing ? \"\" : undefined}\n      onKeyDown={keyDown}\n      {...rest}\n    >\n      {props.children}\n    </div>\n  ));\n};\n\ninterface ImageProps extends ImageDOMProps {\n  className?: string;\n}\n\ninterface SpanProps extends SpanDOMProps {\n  className?: string;\n}\n\ninterface DivProps extends DivDOMProps {\n  className?: string;\n}\n\nexport const AudioPlayerArtwork = (props: ImageProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <img\n      alt=\"\"\n      class={cn(\n        \"bg-muted size-12 shrink-0 rounded-lg object-cover\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-player-artwork\"\n      {...rest}\n    />\n  );\n};\n\nexport const AudioPlayerTitle = (props: SpanProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <span\n      class={cn(\"truncate text-sm font-medium\", props.class, props.className)}\n      data-slot=\"audio-player-title\"\n      {...rest}\n    />\n  );\n};\n\nexport const AudioPlayerDescription = (props: SpanProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <span\n      class={cn(\n        \"text-muted-foreground truncate text-xs\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-player-description\"\n      {...rest}\n    />\n  );\n};\n\nexport const AudioPlayerControls = (props: DivProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <div\n      class={cn(\"flex items-center gap-1\", props.class, props.className)}\n      data-slot=\"audio-player-controls\"\n      {...rest}\n    />\n  );\n};\n\nconst BUTTON_CLASS =\n  \"inline-flex size-8 shrink-0 items-center justify-center rounded-full text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-40 [&_svg:not([class*='size-'])]:size-4\";\n\nexport interface PlayerButtonState {\n  readonly slot: string;\n  readonly playing?: boolean;\n  readonly loading?: boolean;\n  readonly looping?: boolean;\n}\n\nexport interface PlayerButtonRenderProps extends ButtonDOMProps {\n  \"data-slot\": string;\n  \"data-playing\"?: string;\n  \"data-loading\"?: string;\n  \"data-looping\"?: string;\n  onClick: (event: MouseEvent) => void;\n}\n\nexport interface PlayerButtonProps extends Omit<ButtonDOMProps, \"onClick\"> {\n  className?: string;\n  onClick?: (event: MouseEvent) => void;\n  render?: (\n    props: PlayerButtonRenderProps,\n    state: PlayerButtonState\n  ) => JSXElement;\n}\n\ninterface ButtonProps extends PlayerButtonProps {\n  slotName: string;\n  label: string;\n  action: () => void;\n  playing?: boolean;\n  loading?: boolean;\n  looping?: boolean;\n}\n\nconst PlayerButton = (props: ButtonProps) => {\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"slotName\",\n    \"label\",\n    \"action\",\n    \"onClick\",\n    \"children\",\n    \"render\",\n    \"playing\",\n    \"loading\",\n    \"looping\",\n  ]);\n\n  const state: PlayerButtonState = {\n    get slot() {\n      return props.slotName;\n    },\n    get playing() {\n      return props.playing;\n    },\n    get loading() {\n      return props.loading;\n    },\n    get looping() {\n      return props.looping;\n    },\n  };\n\n  const renderProps: PlayerButtonRenderProps = {\n    get \"aria-label\"() {\n      return props.label;\n    },\n    get class() {\n      return cn(\n        BUTTON_CLASS,\n        !props.children && \"w-auto px-2.5\",\n        props.class,\n        props.className\n      );\n    },\n    type: \"button\",\n    get \"data-slot\"() {\n      return props.slotName;\n    },\n    get \"data-playing\"() {\n      return props.playing ? \"\" : undefined;\n    },\n    get \"data-loading\"() {\n      return props.loading ? \"\" : undefined;\n    },\n    get \"data-looping\"() {\n      return props.looping ? \"\" : undefined;\n    },\n    onClick: (event: MouseEvent) => {\n      props.onClick?.(event);\n\n      if (!event.defaultPrevented) props.action();\n    },\n    get children() {\n      return props.children ?? <span class=\"text-xs\">{props.label}</span>;\n    },\n  };\n\n  forwardProps(renderProps, rest);\n  const render = untrack(() => props.render);\n\n  if (render) {\n    const rendered = createMemo(() => render(renderProps, state));\n\n    return <>{rendered()}</>;\n  }\n\n  return <button {...renderProps} />;\n};\n\ninterface PlayState {\n  playing: boolean;\n  loading: boolean;\n}\n\nexport interface AudioPlayerPlayProps extends Omit<\n  PlayerButtonProps,\n  \"children\"\n> {\n  children?: JSXElement | ((state: PlayState) => JSXElement);\n}\n\nconst isPlayRenderer = (\n  children: AudioPlayerPlayProps[\"children\"]\n): children is (state: PlayState) => JSXElement =>\n  typeof children === \"function\";\n\nexport const AudioPlayerPlay = (props: AudioPlayerPlayProps) => {\n  const settings = usePlayerPart(\"AudioPlayerPlay\");\n  const rest = omitProps(props, [\"children\", \"class\", \"className\"]);\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-play\"\n      label={settings.player.playing ? \"Pause\" : \"Play\"}\n      action={() => void settings.player.toggle()}\n      class={cn(\n        \"bg-primary text-primary-foreground hover:bg-primary/85 size-10\",\n        props.class,\n        props.className\n      )}\n      disabled={\n        settings.player.status === \"idle\" || settings.player.status === \"error\"\n      }\n      data-loading={settings.player.status === \"loading\" ? \"\" : undefined}\n      data-playing={settings.player.playing ? \"\" : undefined}\n      loading={settings.player.status === \"loading\"}\n      playing={settings.player.playing}\n      {...rest}\n    >\n      {isPlayRenderer(props.children)\n        ? props.children({\n            get playing() {\n              return settings.player.playing;\n            },\n            get loading() {\n              return settings.player.status === \"loading\";\n            },\n          })\n        : props.children}\n    </PlayerButton>\n  );\n};\n\nexport const AudioPlayerPrevious = (props: PlayerButtonProps) => {\n  const settings = usePlayerPart(\"AudioPlayerPrevious\");\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-previous\"\n      label=\"Previous\"\n      action={() => settings.onPrevious?.()}\n      disabled={!settings.onPrevious}\n      {...props}\n    />\n  );\n};\n\nexport const AudioPlayerNext = (props: PlayerButtonProps) => {\n  const settings = usePlayerPart(\"AudioPlayerNext\");\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-next\"\n      label=\"Next\"\n      action={() => settings.onNext?.()}\n      disabled={!settings.onNext}\n      {...props}\n    />\n  );\n};\n\nexport interface AudioPlayerSkipProps extends PlayerButtonProps {\n  seconds?: number;\n}\n\nexport const AudioPlayerSkipBack = (props: AudioPlayerSkipProps) => {\n  const settings = usePlayerPart(\"AudioPlayerSkipBack\");\n  const rest = omitProps(props, [\"seconds\"]);\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-skip-back\"\n      label={`Back ${props.seconds ?? 10} seconds`}\n      action={() =>\n        settings.player.seek(\n          settings.player.currentTime - (props.seconds ?? 10)\n        )\n      }\n      {...rest}\n    />\n  );\n};\n\nexport const AudioPlayerSkipForward = (props: AudioPlayerSkipProps) => {\n  const settings = usePlayerPart(\"AudioPlayerSkipForward\");\n  const rest = omitProps(props, [\"seconds\"]);\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-skip-forward\"\n      label={`Forward ${props.seconds ?? 10} seconds`}\n      action={() =>\n        settings.player.seek(\n          settings.player.currentTime + (props.seconds ?? 10)\n        )\n      }\n      {...rest}\n    />\n  );\n};\n\nconst SeekCompat = (props: { onChangeEnd: () => void }) => {\n  useKobalteSliderCompat(SliderPrimitive.useSliderContext(), {\n    onChangeEnd: () => props.onChangeEnd(),\n    suppressStep: true,\n  });\n\n  return null;\n};\n\ntype SliderProps = Parameters<typeof SliderPrimitive.Root>[0];\n\nexport interface AudioPlayerSeekProps extends Omit<\n  SliderProps,\n  | \"value\"\n  | \"defaultValue\"\n  | \"onChange\"\n  | \"onChangeEnd\"\n  | \"minValue\"\n  | \"maxValue\"\n  | \"step\"\n  | \"children\"\n> {\n  className?: string;\n  step?: number;\n  largeStep?: number;\n}\n\nexport const AudioPlayerSeek = (props: AudioPlayerSeekProps) => {\n  const settings = usePlayerPart(\"AudioPlayerSeek\");\n  const [time, setTime] = createSignal(settings.player.currentTime);\n  const [drag, setDrag] = createSignal<number | null>(null);\n  let latestDrag: number | null = null;\n  useFrameSource(\n    () => settings.player.time,\n    (next) => {\n      if (drag() === null) setTime(next);\n    }\n  );\n  createCompatEffect(\n    () => settings.player.currentTime,\n    (next) => {\n      if (drag() === null) setTime(next);\n    }\n  );\n  const duration = () => settings.player.duration || 0;\n  const value = () => drag() ?? time();\n\n  const sliderValue = createMemo(() => [\n    clamp(value(), 0, Math.max(duration(), 0.001)),\n  ]);\n\n  const seek = (next: number) => {\n    settings.player.seek(next);\n    latestDrag = null;\n    setTime(next);\n    setDrag(null);\n  };\n\n  const keyDown = (event: KeyboardEvent) => {\n    const amount = event.shiftKey\n      ? (props.largeStep ?? SEEK_LARGE_STEP)\n      : (props.step ?? SEEK_STEP);\n\n    const targets = {\n      ArrowRight: value() + amount,\n      ArrowUp: value() + amount,\n      ArrowLeft: value() - amount,\n      ArrowDown: value() - amount,\n      Home: 0,\n      End: duration(),\n    };\n\n    if (!Object.hasOwn(targets, event.key)) return;\n    event.preventDefault();\n    // SAFETY: The own-property check proves this is a seek navigation key.\n    seek(targets[event.key as keyof typeof targets]);\n  };\n\n  const rest = omitProps(props, [\"class\", \"className\", \"step\", \"largeStep\"]);\n\n  return (\n    <SliderPrimitive.Root\n      class={cn(\n        \"relative flex min-w-24 flex-1 touch-none items-center select-none\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-player-seek\"\n      disabled={duration() === 0}\n      minValue={0}\n      maxValue={Math.max(duration(), 0.001)}\n      step={0.01}\n      value={sliderValue()}\n      onChange={(values) => {\n        latestDrag = values[0] ?? 0;\n        setDrag(latestDrag);\n      }}\n      onChangeEnd={(values) => seek(values[0] ?? 0)}\n      getValueLabel={() =>\n        `${formatTime(value())} of ${formatTime(duration())}`\n      }\n      {...rest}\n    >\n      <SeekCompat onChangeEnd={() => seek(latestDrag ?? value())} />\n      <SliderPrimitive.Track class=\"relative flex h-4 w-full items-center px-1.5 before:absolute before:inset-x-0 before:-inset-y-1.5 pointer-coarse:before:-inset-y-3\">\n        <div\n          class=\"bg-input/90 relative h-1 w-full grow rounded-full\"\n          data-slot=\"audio-player-seek-track\"\n        >\n          <div\n            class=\"bg-muted-foreground/25 absolute inset-y-0 left-0 w-(--buffered) rounded-full\"\n            data-slot=\"audio-player-seek-buffered\"\n            style={{\n              \"--buffered\": `${duration() > 0 ? clamp(settings.player.buffered / duration(), 0, 1) * 100 : 0}%`,\n            }}\n          />\n          <div\n            class=\"bg-primary absolute inset-y-0 left-0 rounded-full\"\n            data-slot=\"audio-player-seek-range\"\n            style={{\n              width: `${duration() > 0 ? (value() / duration()) * 100 : 0}%`,\n            }}\n          />\n        </div>\n        <SliderPrimitive.Thumb\n          aria-label=\"Seek\"\n          aria-valuetext={`${formatTime(value())} of ${formatTime(duration())}`}\n          class=\"bg-background ring-foreground/15 hover:ring-ring/30 focus-visible:ring-ring/40 block size-3 shrink-0 rounded-full shadow-sm ring-1 outline-hidden transition-[box-shadow] hover:ring-4 focus-visible:ring-4\"\n          data-slot=\"audio-player-seek-thumb\"\n          onKeyDown={keyDown}\n        >\n          <SliderPrimitive.Input />\n        </SliderPrimitive.Thumb>\n      </SliderPrimitive.Track>\n    </SliderPrimitive.Root>\n  );\n};\n\nexport interface AudioPlayerTimeProps extends Omit<SpanProps, \"style\"> {\n  style?: StyleValue;\n  type?: \"current\" | \"remaining\" | \"duration\";\n  format?: (\n    seconds: number,\n    type: \"current\" | \"remaining\" | \"duration\"\n  ) => string;\n}\n\nconst defaultTimeFormat = (\n  seconds: number,\n  type: \"current\" | \"remaining\" | \"duration\"\n) => formatTime(seconds, { remaining: type === \"remaining\" });\n\nexport const AudioPlayerTime = (props: AudioPlayerTimeProps) => {\n  const settings = usePlayerPart(\"AudioPlayerTime\");\n  const type = () => props.type ?? \"current\";\n\n  const format = (seconds: number) =>\n    (props.format ?? defaultTimeFormat)(seconds, type());\n\n  const seconds = () =>\n    type() === \"duration\"\n      ? settings.player.duration\n      : type() === \"remaining\"\n        ? Math.max(0, settings.player.duration - settings.player.currentTime)\n        : settings.player.currentTime;\n\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"style\",\n    \"type\",\n    \"format\",\n  ]);\n\n  return (\n    <span\n      class={cn(\n        \"text-muted-foreground inline-block min-w-(--audio-player-time-width) text-end font-mono text-xs whitespace-nowrap tabular-nums\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"audio-player-time\"\n      data-type={type()}\n      style={mergeStyleVars(props.style, {\n        \"--audio-player-time-width\": `${Math.max(format(0).length, format(settings.player.duration).length)}ch`,\n      })}\n      {...rest}\n    >\n      {format(seconds())}\n    </span>\n  );\n};\n\nexport type AudioPlayerVolumeProps = Omit<\n  VolumeControlProps,\n  | \"value\"\n  | \"defaultValue\"\n  | \"onValueChange\"\n  | \"muted\"\n  | \"defaultMuted\"\n  | \"onMutedChange\"\n>;\n\nexport const AudioPlayerVolume = (props: AudioPlayerVolumeProps) => {\n  const settings = usePlayerPart(\"AudioPlayerVolume\");\n  const rest = omitProps(props, [\"class\", \"className\", \"children\"]);\n\n  return (\n    <VolumeControl\n      class={cn(\"w-32\", props.class, props.className)}\n      data-slot=\"audio-player-volume\"\n      value={settings.player.volume}\n      muted={settings.player.muted}\n      onValueChange={(next) => settings.player.setVolume(next)}\n      onMutedChange={(next) => settings.player.setMuted(next)}\n      {...rest}\n    >\n      {props.children ?? (\n        <>\n          <VolumeControlMute />\n          <VolumeControlSlider />\n        </>\n      )}\n    </VolumeControl>\n  );\n};\n\nexport interface AudioPlayerRateProps extends PlayerButtonProps {\n  rates?: number[];\n}\n\nexport const AudioPlayerRate = (props: AudioPlayerRateProps) => {\n  const settings = usePlayerPart(\"AudioPlayerRate\");\n  const rates = () => props.rates ?? DEFAULT_RATES;\n  const rest = omitProps(props, [\"class\", \"className\", \"rates\", \"children\"]);\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-rate\"\n      label={`Playback speed ${settings.player.playbackRate}×`}\n      class={cn(\"w-auto px-2\", props.class, props.className)}\n      action={() =>\n        settings.player.setPlaybackRate(\n          rates()[\n            (rates().indexOf(settings.player.playbackRate) + 1) % rates().length\n          ] ?? 1\n        )\n      }\n      {...rest}\n    >\n      {props.children ?? (\n        <span\n          class=\"inline-block min-w-(--rate-width) text-center font-mono text-xs tabular-nums\"\n          style={{\n            \"--rate-width\": `${Math.max(...rates().map((rate) => `${rate}×`.length))}ch`,\n          }}\n        >\n          {settings.player.playbackRate}×\n        </span>\n      )}\n    </PlayerButton>\n  );\n};\n\nexport const AudioPlayerLoop = (props: PlayerButtonProps) => {\n  const settings = usePlayerPart(\"AudioPlayerLoop\");\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <PlayerButton\n      slotName=\"audio-player-loop\"\n      label={settings.player.loop ? \"Loop on\" : \"Loop off\"}\n      action={() => settings.player.setLoop(!settings.player.loop)}\n      aria-pressed={settings.player.loop ? \"true\" : \"false\"}\n      data-looping={settings.player.loop ? \"\" : undefined}\n      looping={settings.player.loop}\n      class={cn(\n        \"aria-pressed:bg-muted aria-pressed:text-primary\",\n        props.class,\n        props.className\n      )}\n      {...rest}\n    />\n  );\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
