{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "music-disc",
  "title": "Map Music Disc",
  "description": "Rotating music disc with floating notes animation.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/music-disc.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport { mapgl } from \"./map-library\"\nimport { cn } from \"@/lib/utils\"\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates } from \"./types\"\n\ntype MusicNote = {\n  id: number\n  offsetX: number\n  delay: number\n  duration: number\n  type: \"note\" | \"eighth\" | \"double\"\n}\n\ntype MapMusicDiscProps = {\n  coordinates: MapCoordinates\n  size?: number\n  image?: string\n  spinning?: boolean\n  spinDuration?: number\n  showNotes?: boolean\n  noteColor?: string\n  discColor?: string\n  centerColor?: string\n  pitchAlignment?: \"map\" | \"viewport\" | \"auto\"\n  className?: string\n}\n\ntype DiscContentProps = {\n  size: number\n  image?: string\n  spinning: boolean\n  spinDuration: number\n  discColor: string\n  centerColor: string\n  className?: string\n}\n\ntype NotesContentProps = {\n  size: number\n  noteColor: string\n  notes: MusicNote[]\n}\n\ntype MusicDiscContentProps = {\n  size: number\n  image?: string\n  spinning: boolean\n  spinDuration: number\n  showNotes: boolean\n  noteColor: string\n  discColor: string\n  centerColor: string\n  notes: MusicNote[]\n  className?: string\n}\n\ntype MusicNoteIconProps = {\n  note: MusicNote\n  color: string\n}\n\nconst DEFAULT_SIZE = 64\nconst DEFAULT_SPINNING = true\nconst DEFAULT_SPIN_DURATION = 4000\nconst DEFAULT_SHOW_NOTES = true\nconst DEFAULT_NOTE_COLOR = \"#a855f7\"\nconst DEFAULT_DISC_COLOR = \"#0a0a0a\"\nconst DEFAULT_CENTER_COLOR = \"#171717\"\nconst DEFAULT_PITCH_ALIGNMENT = \"viewport\" as const\n\nconst NOTE_SYMBOLS = {\n  note: \"♪\",\n  eighth: \"♫\",\n  double: \"♬\",\n} as const\n\nexport const MapMusicDisc = ({\n  coordinates,\n  size = DEFAULT_SIZE,\n  image,\n  spinning = DEFAULT_SPINNING,\n  spinDuration = DEFAULT_SPIN_DURATION,\n  showNotes = DEFAULT_SHOW_NOTES,\n  noteColor = DEFAULT_NOTE_COLOR,\n  discColor = DEFAULT_DISC_COLOR,\n  centerColor = DEFAULT_CENTER_COLOR,\n  pitchAlignment = DEFAULT_PITCH_ALIGNMENT,\n  className,\n}: MapMusicDiscProps) => {\n  const { map, isLoaded } = useMap()\n  const discMarkerRef = useRef<mapboxgl.Marker | null>(null)\n  const notesMarkerRef = useRef<mapboxgl.Marker | null>(null)\n  const discContainerRef = useRef<HTMLDivElement | null>(null)\n  const notesContainerRef = useRef<HTMLDivElement | null>(null)\n  const [isMounted, setIsMounted] = useState(false)\n\n  const needsSeparateMarkers = showNotes && pitchAlignment === \"map\"\n\n  useEffect(() => {\n    if (!map || !isLoaded) {\n      return\n    }\n\n    const discContainer = document.createElement(\"div\")\n    discContainerRef.current = discContainer\n\n    const discMarker = new mapgl.Marker({\n      element: discContainer,\n      anchor: \"center\",\n      pitchAlignment,\n    })\n      .setLngLat(coordinates)\n      .addTo(map)\n\n    discMarkerRef.current = discMarker\n\n    if (needsSeparateMarkers) {\n      const notesContainer = document.createElement(\"div\")\n      notesContainerRef.current = notesContainer\n\n      const notesMarker = new mapgl.Marker({\n        element: notesContainer,\n        anchor: \"center\",\n        pitchAlignment: \"viewport\",\n      })\n        .setLngLat(coordinates)\n        .addTo(map)\n\n      notesMarkerRef.current = notesMarker\n    }\n\n    setIsMounted(true)\n\n    return () => {\n      discMarker.remove()\n      discMarkerRef.current = null\n      discContainerRef.current = null\n\n      if (notesMarkerRef.current) {\n        notesMarkerRef.current.remove()\n        notesMarkerRef.current = null\n        notesContainerRef.current = null\n      }\n\n      setIsMounted(false)\n    }\n  }, [map, isLoaded, needsSeparateMarkers, pitchAlignment])\n\n  useEffect(() => {\n    if (discMarkerRef.current) {\n      discMarkerRef.current.setLngLat(coordinates)\n    }\n    if (notesMarkerRef.current) {\n      notesMarkerRef.current.setLngLat(coordinates)\n    }\n  }, [coordinates])\n\n  if (!isMounted || !discContainerRef.current) {\n    return null\n  }\n\n  if (needsSeparateMarkers) {\n    return (\n      <>\n        {createPortal(\n          <DiscContent\n            size={size}\n            image={image}\n            spinning={spinning}\n            spinDuration={spinDuration}\n            discColor={discColor}\n            centerColor={centerColor}\n            className={className}\n          />,\n          discContainerRef.current\n        )}\n        {notesContainerRef.current &&\n          createPortal(<NotesContent size={size} noteColor={noteColor} notes={NOTES} />, notesContainerRef.current)}\n      </>\n    )\n  }\n\n  return createPortal(\n    <MusicDiscContent\n      size={size}\n      image={image}\n      spinning={spinning}\n      spinDuration={spinDuration}\n      showNotes={showNotes}\n      noteColor={noteColor}\n      discColor={discColor}\n      centerColor={centerColor}\n      notes={NOTES}\n      className={className}\n    />,\n    discContainerRef.current\n  )\n}\n\nexport const MusicDisc = ({\n  size = DEFAULT_SIZE,\n  image,\n  spinning = DEFAULT_SPINNING,\n  spinDuration = DEFAULT_SPIN_DURATION,\n  showNotes = DEFAULT_SHOW_NOTES,\n  noteColor = DEFAULT_NOTE_COLOR,\n  discColor = DEFAULT_DISC_COLOR,\n  centerColor = DEFAULT_CENTER_COLOR,\n  className,\n}: Omit<MapMusicDiscProps, \"coordinates\" | \"pitchAlignment\">) => {\n  return (\n    <MusicDiscContent\n      size={size}\n      image={image}\n      spinning={spinning}\n      spinDuration={spinDuration}\n      showNotes={showNotes}\n      noteColor={noteColor}\n      discColor={discColor}\n      centerColor={centerColor}\n      notes={NOTES}\n      className={className}\n    />\n  )\n}\n\nconst MusicDiscContent = ({\n  size,\n  image,\n  spinning,\n  spinDuration,\n  showNotes,\n  noteColor,\n  discColor,\n  centerColor,\n  notes,\n  className,\n}: MusicDiscContentProps) => {\n  return (\n    <div className={cn(\"relative\", className)} style={{ width: size, height: size }}>\n      {showNotes && (\n        <div style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}>\n          {notes.map((note) => {\n            return <MusicNoteIcon key={note.id} note={note} color={noteColor} />\n          })}\n        </div>\n      )}\n      <DiscContent\n        size={size}\n        image={image}\n        spinning={spinning}\n        spinDuration={spinDuration}\n        discColor={discColor}\n        centerColor={centerColor}\n      />\n    </div>\n  )\n}\n\nconst NotesContent = ({ size, noteColor, notes }: NotesContentProps) => {\n  return (\n    <div style={{ position: \"relative\", width: size, height: size }}>\n      <div style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}>\n        {notes.map((note) => {\n          return <MusicNoteIcon key={note.id} note={note} color={noteColor} />\n        })}\n      </div>\n    </div>\n  )\n}\n\nconst DiscContent = ({ size, image, spinning, spinDuration, discColor, centerColor, className }: DiscContentProps) => {\n  const grooveCount = Math.floor(size / 8)\n  const centerSize = size * 0.35\n  const holeSize = size * 0.08\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"relative\",\n        width: size,\n        height: size,\n        borderRadius: \"50%\",\n        backgroundColor: discColor,\n        boxShadow: \"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)\",\n        overflow: \"hidden\",\n        animation: spinning ? `music-disc-spin ${spinDuration}ms linear infinite` : \"none\",\n      }}\n    >\n      {Array.from({ length: grooveCount }, (_, grooveIndex) => {\n        const grooveRadius = ((grooveIndex + 1) / grooveCount) * 45\n        return (\n          <div\n            key={grooveIndex}\n            style={{\n              position: \"absolute\",\n              borderRadius: \"50%\",\n              border: \"1px solid rgba(255, 255, 255, 0.05)\",\n              width: `${grooveRadius * 2}%`,\n              height: `${grooveRadius * 2}%`,\n              top: `${50 - grooveRadius}%`,\n              left: `${50 - grooveRadius}%`,\n            }}\n          />\n        )\n      })}\n\n      <div\n        style={{\n          position: \"absolute\",\n          borderRadius: \"50%\",\n          boxShadow: \"inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          overflow: \"hidden\",\n          width: centerSize,\n          height: centerSize,\n          top: (size - centerSize) / 2,\n          left: (size - centerSize) / 2,\n          backgroundColor: centerColor,\n        }}\n      >\n        {image ? (\n          <img src={image} alt=\"Album cover\" style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }} />\n        ) : (\n          <div\n            style={{\n              borderRadius: \"50%\",\n              background: \"linear-gradient(to bottom right, #262626, #0a0a0a)\",\n              width: centerSize * 0.8,\n              height: centerSize * 0.8,\n            }}\n          />\n        )}\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          borderRadius: \"50%\",\n          backgroundColor: \"black\",\n          width: holeSize,\n          height: holeSize,\n          top: (size - holeSize) / 2,\n          left: (size - holeSize) / 2,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          borderRadius: \"50%\",\n          background: \"linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%, rgba(0,0,0,0.2) 100%)\",\n        }}\n      />\n    </div>\n  )\n}\n\nconst MusicNoteIcon = ({ note, color }: MusicNoteIconProps) => {\n  return (\n    <span\n      style={{\n        position: \"absolute\",\n        left: `calc(50% + ${note.offsetX}px)`,\n        color,\n        fontSize: \"18px\",\n        fontWeight: \"bold\",\n        pointerEvents: \"none\",\n        userSelect: \"none\",\n        animation: `music-disc-float ${note.duration}ms ease-out infinite`,\n        animationDelay: `${note.delay}ms`,\n      }}\n    >\n      {NOTE_SYMBOLS[note.type]}\n    </span>\n  )\n}\n\nconst NOTES: MusicNote[] = [\n  { id: 0, offsetX: -12, delay: 0, duration: 2400, type: \"note\" },\n  { id: 1, offsetX: 8, delay: 500, duration: 2800, type: \"eighth\" },\n  { id: 2, offsetX: -6, delay: 1000, duration: 2200, type: \"double\" },\n  { id: 3, offsetX: 14, delay: 1500, duration: 2600, type: \"note\" },\n  { id: 4, offsetX: -16, delay: 2000, duration: 2400, type: \"eighth\" },\n  { id: 5, offsetX: 4, delay: 2500, duration: 3000, type: \"double\" },\n]\n",
      "type": "registry:ui",
      "target": "components/ui/map/music-disc.tsx"
    }
  ],
  "type": "registry:ui"
}
