{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "audio-player",
  "type": "registry:component",
  "title": "Lens Audio Player",
  "description": "An audio player component for playing audio attachments in Lens posts.",
  "dependencies": [
    "@lens-protocol/react@0.0.0-canary-20250820102520",
    "lucide-react",
    "react-player@3.3.2",
    "@radix-ui/react-slider@1.3.6"
  ],
  "registryDependencies": ["avatar", "button", "https://lensblocks.com/r/utils.json"],
  "files": [
    {
      "path": "registry/new-york/components/common/lens-audio-player.tsx",
      "content": "import ReactPlayer from \"react-player\";\nimport { MediaAudio } from \"@lens-protocol/react\";\nimport { getAudioExtension, parseUri } from \"@/registry/new-york/lib/lens-utils\";\nimport { SyntheticEvent, useRef, useState } from \"react\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport { PauseIcon, PlayIcon, Volume2Icon, VolumeOffIcon } from \"lucide-react\";\nimport { MediaSeekSlider } from \"@/registry/new-york/ui/media-seek-slider\";\nimport { Duration } from \"@/registry/new-york/ui/duration\";\nimport { Skeleton } from \"@/registry/new-york/ui/skeleton\";\n\ntype Props = {\n  audio: MediaAudio;\n  postTitle?: string | null;\n  preload?: \"none\" | \"metadata\" | \"auto\" | \"\";\n  onError?: (e: any) => void;\n  onCoverClick?: (imageUri: string) => void;\n};\n\nexport const LensAudioPlayer = (props: Props) => {\n  const { audio, postTitle, preload = \"metadata\", onError } = props;\n\n  // ReactPlayer looks at the file extension in the source URI to determine how to play the file,\n  // so we need to append the extension to the url for instances where the audio url does not have an extension\n  // e.g., when using Grove urls like https://api.grove.storage/<key>\n  // or when using a proxy server that does not preserve the original file name\n  const audioUri = audio ? parseUri(audio.item) + \"?extension=.\" + getAudioExtension(audio.type) : null;\n\n  const playerRef = useRef<HTMLVideoElement | null>(null);\n\n  const initialState = {\n    playing: false,\n    volume: 1,\n    muted: false,\n    played: 0,\n    loaded: 0,\n    duration: 0,\n    seeking: false,\n    loadedSeconds: 0,\n    playedSeconds: 0,\n  };\n\n  type PlayerState = typeof initialState;\n\n  const [state, setState] = useState<PlayerState>(initialState);\n  const [showTimeRemaining, setShowTimeRemaining] = useState(true);\n  const [hasPlayed, setHasPlayed] = useState(false);\n\n  if (!audio || !audioUri) {\n    return null;\n  }\n\n  if (!ReactPlayer.canPlay?.(audioUri)) {\n    return <audio controls src={audioUri} />;\n  }\n\n  const handlePlayPause = () => {\n    setState(prevState => ({ ...prevState, playing: !prevState.playing }));\n  };\n\n  const handleToggleMuted = () => {\n    setState(prevState => ({ ...prevState, muted: !prevState.muted }));\n  };\n\n  const handlePlay = () => {\n    setState(prevState => ({ ...prevState, playing: true }));\n    setHasPlayed(true);\n  };\n\n  const handlePause = () => {\n    setState(prevState => ({ ...prevState, playing: false }));\n  };\n\n  const handleSeekMouseDown = () => {\n    setState(prevState => ({ ...prevState, seeking: true }));\n  };\n\n  const handleSeekChange = (event: SyntheticEvent<HTMLInputElement>) => {\n    const inputTarget = event.target as HTMLInputElement;\n    setState(prevState => ({ ...prevState, played: Number.parseFloat(inputTarget.value) }));\n  };\n\n  const onValueChange = (values: Number[]) => {\n    setState(prevState => ({ ...prevState, seeking: false }));\n    if (playerRef.current) {\n      playerRef.current.currentTime = values[0].valueOf() * playerRef.current.duration;\n    }\n  };\n\n  const handleProgress = () => {\n    const player = playerRef.current;\n    // We only want to update time slider if we are not currently seeking\n    if (!player || state.seeking || !player.buffered?.length) return;\n\n    setState(prevState => ({\n      ...prevState,\n      loadedSeconds: player.buffered?.end(player.buffered?.length - 1),\n      loaded: player.buffered?.end(player.buffered?.length - 1) / player.duration,\n    }));\n  };\n\n  const handleTimeUpdate = () => {\n    const player = playerRef.current;\n    // We only want to update time slider if we are not currently seeking\n    if (!player || state.seeking) return;\n\n    if (!player.duration) return;\n\n    setState(prevState => ({\n      ...prevState,\n      playedSeconds: player.currentTime,\n      played: player.currentTime / player.duration,\n    }));\n  };\n\n  const handleEnded = () => {\n    setState(prevState => ({ ...prevState, playing: false }));\n  };\n\n  const handleDurationChange = () => {\n    const player = playerRef.current;\n    if (!player) return;\n\n    setState(prevState => ({ ...prevState, duration: player.duration }));\n  };\n\n  const { playing, playedSeconds, volume, muted, played, loaded, duration } = state;\n\n  return (\n    <div\n      className=\"w-full flex border rounded-xl mt-1 bg-card text-card-foreground overflow-hidden\"\n      onClick={event => event.stopPropagation()}\n    >\n      <div className=\"w-full h-24 flex items-center\">\n        {audio.cover && (\n          <img\n            src={parseUri(audio.cover)!!}\n            alt=\"Cover image\"\n            className=\"aspect-square object-cover h-full rounded-l-xl flex-1 cursor-pointer hover:opacity-90\"\n            width={192}\n            height={192}\n            loading={\"lazy\"}\n            onClick={() => props.onCoverClick?.(parseUri(audio.cover)!!)}\n          />\n        )}\n\n        <div className=\"w-full min-w-0 flex flex-col h-full px-2 justify-center\">\n          <div className=\"w-full min-w-0 flex flex-col pl-1 gap-1 md:gap-0\">\n            {(audio.title || postTitle) && (\n              <div className=\"text-sm md:text-lg font-semibold truncate mt-1\">{audio.title ?? postTitle}</div>\n            )}\n            {audio.artist && <div className=\"text-sm md:text-base opacity-80 truncate -mt-1\">{audio.artist}</div>}\n          </div>\n          <ReactPlayer\n            ref={playerRef}\n            src={audioUri}\n            controls={false}\n            preload={preload}\n            playing={playing}\n            volume={volume}\n            muted={muted}\n            onPlay={handlePlay}\n            onPause={handlePause}\n            onEnded={handleEnded}\n            onError={onError}\n            onTimeUpdate={handleTimeUpdate}\n            onProgress={handleProgress}\n            onDurationChange={handleDurationChange}\n          />\n          <div className=\"w-full flex gap-1 items-center\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"-ms-2 flex-none rounded-full\"\n              onClick={handlePlayPause}\n              disabled={!loaded}\n            >\n              {playing ? <PauseIcon fill=\"var(--primary)\" /> : <PlayIcon fill=\"var(--primary)\" />}\n            </Button>\n            <MediaSeekSlider\n              min={0}\n              max={0.999999}\n              step={0.01}\n              value={[played]}\n              onValueChange={onValueChange}\n              onMouseDown={handleSeekMouseDown}\n              onChange={handleSeekChange}\n              className=\"flex-grow\"\n              disabled={!loaded}\n            />\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              onClick={() => setShowTimeRemaining(!showTimeRemaining)}\n              className=\"flex-none rounded-full\"\n              disabled={!loaded}\n            >\n              <Duration\n                seconds={!hasPlayed ? duration : showTimeRemaining ? duration - playedSeconds : playedSeconds}\n                isCountdown={hasPlayed && showTimeRemaining}\n                className=\"flex-none text-sm\"\n              />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"flex-none rounded-full\"\n              onClick={handleToggleMuted}\n              disabled={!loaded}\n            >\n              {muted ? <VolumeOffIcon fill=\"var(--primary)\" /> : <Volume2Icon fill=\"var(--primary)\" />}\n            </Button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport const LensAudioPlayerSkeleton = () => {\n  return (\n    <div className=\"w-full flex border rounded-xl mt-1\">\n      <div className=\"w-full h-24 flex items-center\">\n        <Skeleton className=\"aspect-square h-full rounded-l-xl rounded-r-none flex-1\" />\n        <div className=\"w-full flex flex-col h-full px-2 justify-center gap-3\">\n          <div className=\"w-full flex flex-col pl-1 gap-2\">\n            <Skeleton className=\"h-4 w-32\" />\n            <Skeleton className=\"h-3 w-24\" />\n          </div>\n          <div className=\"w-full flex gap-2 items-center px-1\">\n            <Skeleton className=\"h-5 w-5 rounded-full\" />\n            <Skeleton className=\"h-3 flex-grow rounded-full\" />\n            <Skeleton className=\"h-4 w-9 rounded-full\" />\n            <Skeleton className=\"h-5 w-5 rounded-full ml-2\" />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "registry/new-york/ui/media-seek-slider.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction MediaSeekSlider({\n  className,\n  defaultValue,\n  value,\n  min = 0,\n  max = 100,\n  ...props\n}: React.ComponentProps<typeof SliderPrimitive.Root>) {\n  const _values = React.useMemo(\n    () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),\n    [value, defaultValue, min, max],\n  );\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      defaultValue={defaultValue}\n      value={value}\n      min={min}\n      max={max}\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col group\",\n        className,\n      )}\n      {...props}\n    >\n      <SliderPrimitive.Track\n        data-slot=\"slider-track\"\n        className={cn(\n          \"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\",\n        )}\n      >\n        <SliderPrimitive.Range\n          data-slot=\"slider-range\"\n          className={cn(\"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\")}\n        />\n      </SliderPrimitive.Track>\n      {Array.from({ length: _values.length }, (_, index) => (\n        <SliderPrimitive.Thumb\n          data-slot=\"slider-thumb\"\n          key={index}\n          className=\"border-primary bg-foreground ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 invisible group-hover:visible\"\n        />\n      ))}\n    </SliderPrimitive.Root>\n  );\n}\n\nexport { MediaSeekSlider };\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/new-york/ui/duration.tsx",
      "content": "export const Duration = ({\n  className,\n  seconds,\n  isCountdown,\n}: {\n  className?: string;\n  seconds: number;\n  isCountdown?: boolean;\n}) => (\n  <time dateTime={`P${Math.round(seconds)}S`} className={className}>\n    {format(seconds, isCountdown)}\n  </time>\n);\n\nfunction format(seconds: number, isCountdown?: boolean) {\n  const date = new Date(seconds * 1000);\n  const hh = date.getUTCHours();\n  const mm = date.getUTCMinutes();\n  const ss = pad(date.getUTCSeconds());\n  if (hh) {\n    return `${isCountdown ? \"-\" : \"\"}${hh}:${pad(mm)}:${ss}`;\n  }\n  return `${isCountdown ? \"-\" : \"\"}${mm}:${ss}`;\n}\n\nfunction pad(string: string | number) {\n  return `0${string}`.slice(-2);\n}\n",
      "type": "registry:ui"
    }
  ]
}
