{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-music-album-cards",
  "title": "Album Cards",
  "description": "A stacked album card row where clicking a card ejects a spinning vinyl disc, with hover lift and neighbor dimming effects. Requires a /api/spotify route.",
  "dependencies": [
    "spotify-url-info"
  ],
  "files": [
    {
      "path": "registry/components/music/album-cards/index.tsx",
      "content": "'use client';\n\nimport { useState, useEffect, useId, type ReactNode } 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 AlbumCardData {\n  id: string;\n  url: string;\n}\n\nexport interface AlbumCardsProps {\n  albums: AlbumCardData[];\n  title?: string;\n  className?: string;\n}\n\n// --- Vinyl Disc SVG ----------------------------------------------------------\nfunction VinylDisc({ isSpinning, uid }: { isSpinning: boolean; uid: string }) {\n  const grooves: ReactNode[] = [];\n  for (let i = 0; i < 36; i++) {\n    const r = 16 + ((43 - 16) / 36) * i;\n    const opacity = i % 4 === 0 ? 0.1 : i % 2 === 0 ? 0.04 : 0.02;\n    grooves.push(\n      <circle\n        key={i}\n        cx=\"55\"\n        cy=\"55\"\n        r={r}\n        fill=\"none\"\n        stroke={`rgba(255,255,255,${opacity})`}\n        strokeWidth={i % 4 === 0 ? '0.6' : '0.25'}\n      />,\n    );\n  }\n\n  const maskId = `vinyl-mask-${uid}`;\n  const fLeft = `vf-l-${uid}`;\n  const fRight = `vf-r-${uid}`;\n  const fTop = `vf-t-${uid}`;\n  const fBot = `vf-b-${uid}`;\n\n  return (\n    <svg\n      viewBox=\"0 0 110 110\"\n      className=\"size-full drop-shadow-[0_8px_24px_rgba(0,0,0,0.8)]\"\n      style={{ animation: isSpinning ? 'spin 3.4s linear infinite' : 'none' }}\n    >\n      <circle cx=\"55\" cy=\"55\" r=\"55\" fill=\"#0c0c0c\" />\n      <circle cx=\"55\" cy=\"55\" r=\"51.5\" fill=\"#0f0f0f\" />\n      <g>{grooves}</g>\n      <mask id={maskId} maskUnits=\"userSpaceOnUse\">\n        <circle cx=\"55\" cy=\"55\" r=\"55\" fill=\"white\" />\n      </mask>\n      <g mask={`url(#${maskId})`}>\n        <g filter={`url(#${fLeft})`} opacity=\"0.38\">\n          <path fill=\"#fff\" d=\"M-14 38l68 19.579L-14 74V38z\" />\n        </g>\n        <g filter={`url(#${fRight})`} opacity=\"0.38\">\n          <path fill=\"#fff\" d=\"M123 38L55 57.579 123 74V38z\" />\n        </g>\n        <g filter={`url(#${fTop})`} opacity=\"0.38\">\n          <path fill=\"#fff\" d=\"M36.5-12.5l19.579 68 16.421-68h-36z\" />\n        </g>\n        <g filter={`url(#${fBot})`} opacity=\"0.38\">\n          <path fill=\"#fff\" d=\"M36.5 124.5l19.579-68 16.421 68h-36z\" />\n        </g>\n      </g>\n      <circle cx=\"55\" cy=\"55\" r=\"13.5\" fill=\"#7a1212\" />\n      <circle cx=\"55\" cy=\"55\" r=\"11.5\" fill=\"#5c0e0e\" />\n      <circle cx=\"55\" cy=\"55\" r=\"9\" fill=\"#4a0b0b\" />\n      <circle cx=\"55\" cy=\"55\" r=\"2.8\" fill=\"#000\" />\n      <defs>\n        <filter\n          id={fLeft}\n          x=\"-30\"\n          y=\"22\"\n          width=\"100\"\n          height=\"68\"\n          filterUnits=\"userSpaceOnUse\"\n        >\n          <feGaussianBlur stdDeviation=\"9\" />\n        </filter>\n        <filter\n          id={fRight}\n          x=\"39\"\n          y=\"22\"\n          width=\"100\"\n          height=\"68\"\n          filterUnits=\"userSpaceOnUse\"\n        >\n          <feGaussianBlur stdDeviation=\"9\" />\n        </filter>\n        <filter\n          id={fTop}\n          x=\"20.5\"\n          y=\"-28.5\"\n          width=\"68\"\n          height=\"100\"\n          filterUnits=\"userSpaceOnUse\"\n        >\n          <feGaussianBlur stdDeviation=\"9\" />\n        </filter>\n        <filter\n          id={fBot}\n          x=\"20.5\"\n          y=\"40.5\"\n          width=\"68\"\n          height=\"100\"\n          filterUnits=\"userSpaceOnUse\"\n        >\n          <feGaussianBlur stdDeviation=\"9\" />\n        </filter>\n      </defs>\n    </svg>\n  );\n}\n\n// --- Single Album Card --------------------------------------------------------\nfunction AlbumCard({\n  album,\n  index,\n  total,\n  isActive,\n  hasNeighborActive,\n  onActivate,\n}: {\n  album: AlbumCardData;\n  index: number;\n  total: number;\n  isActive: boolean;\n  hasNeighborActive: boolean;\n  onActivate: (id: string | null) => void;\n}) {\n  const uid = useId().replace(/:/g, '');\n  const [vinylOut, setVinylOut] = useState(false);\n  const [hovered, setHovered] = useState(false);\n  const [data, setData] = useState<SpotifyData | null>(null);\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    fetch(`/api/spotify?url=${encodeURIComponent(album.url)}`)\n      .then((r) => (r.ok ? r.json() : Promise.reject()))\n      .then((d: SpotifyData) => setData(d))\n      .catch(() => {})\n      .finally(() => setIsLoading(false));\n  }, [album.url]);\n\n  // Retract vinyl when another card becomes active\n  useEffect(() => {\n    if (!isActive) setVinylOut(false);\n  }, [isActive]);\n\n  const stackZ = total - index;\n  const isRaised = hovered || vinylOut;\n\n  const handleClick = () => {\n    const next = !vinylOut;\n    setVinylOut(next);\n    onActivate(next ? album.id : null);\n  };\n\n  if (isLoading) {\n    return (\n      <div\n        className=\"relative shrink-0\"\n        style={{\n          marginLeft: index > 0 ? '-2.75rem' : undefined,\n          zIndex: stackZ,\n        }}\n      >\n        <div className=\"relative z-10\">\n          <div className=\"size-40 animate-pulse rounded-2xl bg-foreground/10\" />\n          <div className=\"h-14 w-40\" />\n        </div>\n      </div>\n    );\n  }\n\n  if (!data) return null;\n\n  return (\n    <div\n      className=\"relative shrink-0\"\n      style={{\n        zIndex: vinylOut ? 200 : stackZ,\n        marginLeft: index > 0 ? '-2.75rem' : undefined,\n      }}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n    >\n      {/* Vinyl disc slides up on click */}\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute top-0 left-1/2 z-0 aspect-square w-[80%]\"\n        style={\n          vinylOut\n            ? {\n                transform: 'translate(-50%, -56%)',\n                opacity: 1,\n                transition:\n                  'transform 0.5s cubic-bezier(0.34, 1.15, 0.64, 1), opacity 0.25s ease',\n              }\n            : {\n                transform: 'translate(-50%, 5%)',\n                opacity: 0,\n                transition:\n                  'transform 0.3s cubic-bezier(0.32, 0, 0.67, 0), opacity 0.2s ease 0.08s',\n              }\n        }\n      >\n        <VinylDisc isSpinning={vinylOut} uid={uid} />\n      </div>\n\n      {/* Card body */}\n      <div\n        className=\"relative z-10 cursor-pointer\"\n        style={{\n          transform: isRaised ? 'translateY(-14px)' : 'translateY(0px)',\n          transition: 'transform 0.35s cubic-bezier(0.34, 1.4, 0.64, 1)',\n        }}\n        onClick={handleClick}\n      >\n        {/* Album cover */}\n        <div\n          className={cn(\n            'relative size-40 overflow-hidden rounded-2xl',\n            'shadow-[0_8px_28px_rgba(0,0,0,0.14)] dark:shadow-[0_8px_28px_rgba(0,0,0,0.6)]',\n            isRaised &&\n              'shadow-[0_22px_48px_rgba(0,0,0,0.22)] dark:shadow-[0_22px_48px_rgba(0,0,0,0.82)]',\n          )}\n        >\n          <img\n            src={data.image}\n            alt={data.title}\n            draggable={false}\n            className=\"pointer-events-none size-full object-cover select-none\"\n          />\n          {/* Edge vignette */}\n          <div className=\"pointer-events-none absolute inset-0 rounded-2xl shadow-[inset_0_0_12px_rgba(0,0,0,0.4)]\" />\n          {/* Neighbor dimming overlay */}\n          <div\n            className=\"pointer-events-none absolute inset-0 rounded-2xl bg-black/0 transition-[background-color] duration-300\"\n            style={{\n              backgroundColor: hasNeighborActive\n                ? 'rgba(0,0,0,0.125)'\n                : 'rgba(0,0,0,0)',\n            }}\n          />\n        </div>\n\n        {/* Reflection */}\n        <div\n          className=\"pointer-events-none mt-0 h-14 w-40 overflow-hidden\"\n          style={{\n            WebkitMaskImage:\n              'linear-gradient(to bottom, rgba(0,0,0,0.28) 0%, transparent 72%)',\n            maskImage:\n              'linear-gradient(to bottom, rgba(0,0,0,0.28) 0%, transparent 72%)',\n          }}\n        >\n          <img\n            src={data.image}\n            alt=\"\"\n            aria-hidden\n            draggable={false}\n            className=\"pointer-events-none w-40 object-cover select-none\"\n            style={{\n              transform: 'scaleY(-1)',\n              transformOrigin: 'top',\n              height: '10rem',\n            }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// --- AlbumCards Container -----------------------------------------------------\nexport function AlbumCards({\n  albums,\n  title = \"Music I'm Vibin' to\",\n  className,\n}: AlbumCardsProps) {\n  const [activeId, setActiveId] = useState<string | null>(null);\n\n  return (\n    <div className={cn('relative w-full', className)}>\n      <div className=\"flex justify-center [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n        <div className=\"flex shrink-0 items-start pt-24 pb-4\">\n          {albums.map((album, i) => (\n            <AlbumCard\n              key={album.id}\n              album={album}\n              index={i}\n              total={albums.length}\n              isActive={activeId === album.id}\n              hasNeighborActive={activeId !== null && activeId !== album.id}\n              onActivate={setActiveId}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/odysseyui/album-cards.tsx"
    }
  ],
  "css": {
    "@keyframes spin": {
      "from": {
        "transform": "rotate(0deg)"
      },
      "to": {
        "transform": "rotate(360deg)"
      }
    }
  },
  "type": "registry:component"
}