{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-music-vertical-spotify-card-2",
  "title": "Vertical Spotify Card 2",
  "description": "A vertical Spotify music card with a color-extracted blurred gradient background, album art, seek bar, and transport controls. Requires a /api/spotify route.",
  "dependencies": [
    "spotify-url-info"
  ],
  "files": [
    {
      "path": "registry/components/music/vertical-spotify-card-2/index.tsx",
      "content": "'use client';\n\nimport { useState, useEffect, useRef } from 'react';\nimport { cn } from '@/lib/utils';\n\ninterface SpotifyData {\n  title: string;\n  artist: string;\n  image: string;\n  link: string;\n  audio?: string;\n}\n\nexport interface VerticalSpotifyCard2Props {\n  url: string;\n  className?: string;\n  onPrev?: () => void;\n  onNext?: () => void;\n}\n\nfunction formatTime(seconds: number) {\n  if (!isFinite(seconds) || seconds < 0) return '0:00';\n  const m = Math.floor(seconds / 60);\n  const s = Math.floor(seconds % 60);\n  return `${m}:${s.toString().padStart(2, '0')}`;\n}\n\nexport function VerticalSpotifyCard2({\n  url,\n  className,\n  onPrev,\n  onNext,\n}: VerticalSpotifyCard2Props) {\n  const [data, setData] = useState<SpotifyData | null>(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const [error, setError] = useState(false);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [currentTime, setCurrentTime] = useState(0);\n  const [duration, setDuration] = useState(0);\n  const audioRef = useRef<HTMLAudioElement | null>(null);\n\n  useEffect(() => {\n    let cancelled = false;\n\n    if (audioRef.current) {\n      audioRef.current.pause();\n      audioRef.current = null;\n    }\n    setIsPlaying(false);\n    setCurrentTime(0);\n    setDuration(0);\n    setIsLoading(true);\n    setError(false);\n\n    fetch(`/api/spotify?url=${encodeURIComponent(url)}`)\n      .then((r) => (r.ok ? r.json() : Promise.reject()))\n      .then((d) => {\n        if (!cancelled) setData(d);\n      })\n      .catch(() => {\n        if (!cancelled) setError(true);\n      })\n      .finally(() => {\n        if (!cancelled) setIsLoading(false);\n      });\n\n    return () => {\n      cancelled = true;\n      audioRef.current?.pause();\n      audioRef.current = null;\n    };\n  }, [url]);\n\n  const handlePlayPause = () => {\n    if (!data?.audio) return;\n    if (!audioRef.current) {\n      audioRef.current = new Audio(data.audio);\n      audioRef.current.volume = 0.3;\n      audioRef.current.addEventListener('ended', () => {\n        setIsPlaying(false);\n        setCurrentTime(0);\n      });\n      audioRef.current.addEventListener('timeupdate', () => {\n        setCurrentTime(audioRef.current?.currentTime ?? 0);\n      });\n      audioRef.current.addEventListener('loadedmetadata', () => {\n        setDuration(audioRef.current?.duration ?? 0);\n      });\n    }\n    if (isPlaying) {\n      audioRef.current.pause();\n      setIsPlaying(false);\n    } else {\n      audioRef.current.play();\n      setIsPlaying(true);\n    }\n  };\n\n  const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const time = Number(e.target.value);\n    setCurrentTime(time);\n    if (audioRef.current) {\n      audioRef.current.currentTime = time;\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <div\n        className={cn(\n          'relative h-130 w-84 overflow-hidden rounded-3xl bg-neutral-900',\n          className,\n        )}\n      >\n        <div className=\"absolute inset-0 animate-pulse\">\n          <div className=\"h-full w-full bg-linear-to-br from-white/8 via-transparent to-white/4\" />\n        </div>\n        <div className=\"absolute inset-0 flex flex-col gap-5 px-6 pt-6 pb-8\">\n          <div className=\"aspect-square w-full rounded-2xl bg-white/10\" />\n          <div className=\"mt-auto flex w-full flex-col gap-4\">\n            <div className=\"h-5 w-44 rounded bg-white/10\" />\n            <div className=\"h-3.5 w-28 rounded bg-white/10\" />\n            <div className=\"mt-1 h-0.5 w-full rounded bg-white/10\" />\n            <div className=\"h-3 w-20 self-start rounded bg-white/10\" />\n            <div className=\"mt-1 flex items-center justify-center gap-7\">\n              <div className=\"size-8 rounded bg-white/10\" />\n              <div className=\"size-14 rounded-full bg-white/10\" />\n              <div className=\"size-8 rounded bg-white/10\" />\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  if (error || !data) {\n    return (\n      <div\n        className={cn(\n          'flex h-130 w-84 items-center justify-center rounded-3xl border border-border bg-muted/50 text-muted-foreground',\n          className,\n        )}\n      >\n        <p className=\"text-sm\">Failed to load Spotify data</p>\n      </div>\n    );\n  }\n\n  const seekProgress = duration ? (currentTime / duration) * 100 : 0;\n\n  return (\n    <div\n      className={cn(\n        'relative h-130 w-84 overflow-hidden rounded-3xl select-none',\n        className,\n      )}\n    >\n      {/* Background: color-extracted from album art */}\n      <div className=\"pointer-events-none absolute inset-0 z-0\">\n        <img\n          src={data.image}\n          alt=\"\"\n          aria-hidden\n          className=\"absolute top-0 left-0 block h-full w-full object-cover\"\n          style={{\n            filter: 'blur(50px)',\n            transform: 'scale(1.15)',\n            opacity: 0.9,\n          }}\n        />\n        <div className=\"absolute inset-0 bg-black/45\" />\n        <div className=\"absolute inset-0 bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,rgba(0,0,0,0.7)_100%)]\" />\n      </div>\n\n      {/* Content */}\n      <div className=\"relative z-10 flex h-full flex-col px-6\">\n        {/* Album art */}\n        <div className=\"pt-6\">\n          <div\n            className=\"aspect-square w-full overflow-hidden rounded-2xl\"\n            style={{\n              boxShadow:\n                '0 24px 64px rgba(0,0,0,0.65), 0 0 0 1px rgba(255,255,255,0.07)',\n            }}\n          >\n            <img\n              src={data.image}\n              alt={data.title}\n              className=\"h-full w-full object-cover\"\n              style={{\n                transition: 'transform 0.4s cubic-bezier(0.34,1.56,0.64,1)',\n                transform: isPlaying ? 'scale(1.04)' : 'scale(1)',\n              }}\n            />\n          </div>\n        </div>\n\n        {/* Bottom content */}\n        <div className=\"mt-5 flex flex-col gap-3 pb-8\">\n          <div>\n            <h2 className=\"truncate text-[19px] leading-tight font-semibold tracking-[-0.022em] text-white\">\n              {data.title}\n            </h2>\n            <p className=\"mt-0.5 truncate text-[14px] tracking-[-0.01em] text-white/60\">\n              {data.artist}\n            </p>\n          </div>\n\n          {/* Seek bar */}\n          <div className=\"mt-0.5 flex flex-col gap-1.5\">\n            <input\n              type=\"range\"\n              className=\"spotify-seek w-full\"\n              min={0}\n              max={duration || 100}\n              step={0.05}\n              value={currentTime}\n              onChange={handleSeek}\n              disabled={!data.audio}\n              aria-label=\"Seek\"\n              style={{\n                background: `linear-gradient(to right, rgba(255,255,255,0.88) ${seekProgress}%, rgba(255,255,255,0.22) ${seekProgress}%)`,\n              }}\n            />\n            <div className=\"flex justify-between\">\n              <span className=\"font-mono text-[10px] text-white/45 tabular-nums\">\n                {formatTime(currentTime)}\n              </span>\n              <span className=\"font-mono text-[10px] text-white/45 tabular-nums\">\n                {formatTime(duration)}\n              </span>\n            </div>\n          </div>\n\n          {/* Transport controls */}\n          <div className=\"mt-0.5 flex items-center justify-center gap-2\">\n            <button\n              onClick={onPrev}\n              disabled={!onPrev}\n              aria-label=\"Previous\"\n              className=\"cursor-pointer rounded-full p-2 text-white/70 transition-all duration-150 hover:bg-white/10 hover:text-white active:scale-90 disabled:cursor-default disabled:opacity-30\"\n              style={{ filter: 'drop-shadow(0 0 4px rgba(255,255,255,0.35))' }}\n            >\n              <svg\n                width=\"20\"\n                height=\"20\"\n                viewBox=\"0 0 17 17\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M15 2.5v12l-9-6 9-6z\" />\n                <rect x=\"1.5\" y=\"2.5\" width=\"2.8\" height=\"12\" rx=\"0.8\" />\n              </svg>\n            </button>\n\n            <button\n              onClick={handlePlayPause}\n              disabled={!data.audio}\n              aria-label={isPlaying ? 'Pause' : 'Play'}\n              className=\"cursor-pointer rounded-full p-2.5 text-white/90 transition-all duration-150 hover:bg-white/12 hover:text-white active:scale-95 disabled:cursor-default disabled:opacity-30\"\n              style={{ filter: 'drop-shadow(0 0 6px rgba(255,255,255,0.5))' }}\n            >\n              {isPlaying ? (\n                <svg\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 20 20\"\n                  fill=\"currentColor\"\n                >\n                  <rect x=\"4\" y=\"3\" width=\"4.5\" height=\"14\" rx=\"1.5\" />\n                  <rect x=\"11.5\" y=\"3\" width=\"4.5\" height=\"14\" rx=\"1.5\" />\n                </svg>\n              ) : (\n                <svg\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 20 20\"\n                  fill=\"currentColor\"\n                >\n                  <path d=\"M5 3.5v13l12-6.5L5 3.5z\" />\n                </svg>\n              )}\n            </button>\n\n            <button\n              onClick={onNext}\n              disabled={!onNext}\n              aria-label=\"Next\"\n              className=\"cursor-pointer rounded-full p-2 text-white/70 transition-all duration-150 hover:bg-white/10 hover:text-white active:scale-90 disabled:cursor-default disabled:opacity-30\"\n              style={{ filter: 'drop-shadow(0 0 4px rgba(255,255,255,0.35))' }}\n            >\n              <svg\n                width=\"20\"\n                height=\"20\"\n                viewBox=\"0 0 17 17\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M2 2.5v12l9-6-9-6z\" />\n                <rect x=\"12.7\" y=\"2.5\" width=\"2.8\" height=\"12\" rx=\"0.8\" />\n              </svg>\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/odysseyui/vertical-spotify-card-2.tsx"
    }
  ],
  "css": {
    ".spotify-seek": {
      "-webkit-appearance": "none",
      "appearance": "none",
      "height": "4px",
      "border-radius": "4px",
      "outline": "none",
      "cursor": "pointer",
      "transition": "opacity 0.15s",
      "&:disabled": {
        "cursor": "default",
        "opacity": "0.4"
      },
      "&::-webkit-slider-thumb": {
        "-webkit-appearance": "none",
        "appearance": "none",
        "width": "10px",
        "height": "10px",
        "border-radius": "50%",
        "background": "rgba(255, 255, 255, 0.9)",
        "box-shadow": "0 0 6px rgba(255, 255, 255, 0.55)",
        "margin-top": "-4px",
        "transition": "transform 0.15s"
      },
      "&:not(:disabled):hover::-webkit-slider-thumb": {
        "transform": "scale(1.25)"
      },
      "&::-moz-range-thumb": {
        "width": "10px",
        "height": "10px",
        "border-radius": "50%",
        "background": "rgba(255, 255, 255, 0.9)",
        "border": "none",
        "box-shadow": "0 0 6px rgba(255, 255, 255, 0.55)"
      },
      "&::-moz-range-track": {
        "height": "2px",
        "border-radius": "1px"
      }
    },
    "@keyframes spin": {
      "from": {
        "transform": "rotate(0deg)"
      },
      "to": {
        "transform": "rotate(360deg)"
      }
    }
  },
  "type": "registry:component"
}