Luces de Madrid

Noche Temprana

React + MotionMikrointeraktionen

Vinyl mit Play/Pause

Tipp auf die Platte: Play wird zu Pause, der Tonarm senkt sich und das Vinyl beschleunigt und bremst mit Trägheit.

Einstellungen

Code

import { useEffect, useState } from "react";
import { motion, useAnimationFrame, useMotionValue, useReducedMotion, useSpring } from "framer-motion";

const PLAY_L = "M8 5.5 L13.5 8.75 L13.5 15.25 L8 18.5 Z";
const PLAY_R = "M13.5 8.75 L19 12 L19 12 L13.5 15.25 Z";
const PAUSE_L = "M7 5.5 L10.5 5.5 L10.5 18.5 L7 18.5 Z";
const PAUSE_R = "M13.5 5.5 L17 5.5 L17 18.5 L13.5 18.5 Z";
const vinylMorph = { type: "spring" as const, stiffness: 420, damping: 30 };

interface VinylPlayerProps {
  title?: string;
  artist?: string;
  color?: string;
}

export default function VinylPlayer({
  title = "Luces de Madrid",
  artist = "Noche Temprana",
  color = "#ff5a36",
}: VinylPlayerProps) {
  const [playing, setPlaying] = useState(false);
  const reduce = useReducedMotion();
  const speed = useSpring(0, { stiffness: 26, damping: 12 });
  const spin = useMotionValue(0);

  useEffect(() => {
    speed.set(playing ? 1 : 0);
  }, [playing, speed]);

  useAnimationFrame((_, delta) => {
    const s = speed.get();
    if (reduce || s < 0.001) return;
    spin.set((spin.get() + delta * 0.2 * s) % 360);
  });

  return (
    <div className="flex flex-col items-center gap-4">
      <div className="relative h-[200px] w-[232px]">
        <motion.button
          type="button"
          aria-label={playing ? "Pausieren " + title : "Abspielen " + title}
          aria-pressed={playing}
          onClick={() => setPlaying((p) => !p)}
          whileTap={{ scale: 0.97 }}
          transition={{ type: "spring", stiffness: 500, damping: 28 }}
          className="absolute left-0 top-2 h-[184px] w-[184px] rounded-full outline-none focus-visible:ring-2 focus-visible:ring-black/25 focus-visible:ring-offset-4 dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black"
          style={{ boxShadow: "0 1px 2px rgba(0,0,0,0.25), 0 14px 34px -10px rgba(0,0,0,0.5)" }}
        >
          <motion.span
            className="absolute inset-0 rounded-full"
            style={{
              rotate: spin,
              background: "repeating-radial-gradient(circle at 50% 50%, #121212 0 1px, #1d1d1d 1px 2.5px)",
            }}
          >
            <span className="absolute inset-[33%] rounded-full" style={{ backgroundColor: color }}>
              <span className="absolute left-1/2 top-[12%] h-[3px] w-5 -translate-x-1/2 rounded-full bg-white/45" />
              <span className="absolute bottom-[14%] left-1/2 h-[2px] w-3 -translate-x-1/2 rounded-full bg-black/20" />
            </span>
          </motion.span>
          <span
            className="pointer-events-none absolute inset-0 rounded-full"
            style={{
              background:
                "conic-gradient(from 30deg, transparent 0deg, rgba(255,255,255,0.13) 30deg, transparent 70deg, transparent 180deg, rgba(255,255,255,0.08) 215deg, transparent 250deg)",
            }}
            aria-hidden="true"
          />
          <span className="pointer-events-none absolute inset-0 rounded-full ring-1 ring-inset ring-white/10" aria-hidden="true" />
          <span className="absolute inset-0 grid place-items-center text-white" aria-hidden="true">
            <svg viewBox="0 0 24 24" className="h-6 w-6 drop-shadow-[0_1px_1px_rgba(0,0,0,0.25)]" fill="currentColor">
              <motion.path initial={false} animate={{ d: playing ? PAUSE_L : PLAY_L }} transition={vinylMorph} />
              <motion.path initial={false} animate={{ d: playing ? PAUSE_R : PLAY_R }} transition={vinylMorph} />
            </svg>
          </span>
        </motion.button>
        <motion.div
          className="pointer-events-none absolute left-[206px] top-[18px] h-0 w-0"
          initial={false}
          animate={{ rotate: playing ? 22 : 0 }}
          transition={
            playing ? { type: "spring", stiffness: 90, damping: 13, delay: 0.12 } : { type: "spring", stiffness: 220, damping: 24 }
          }
          aria-hidden="true"
        >
          <svg className="absolute overflow-visible" style={{ left: -20, top: -24 }} width="40" height="172" viewBox="-20 -24 40 172">
            <rect x="-5" y="-22" width="10" height="13" rx="3" className="fill-[#3a3a3c] dark:fill-[#8e8e93]" />
            <path
              d="M0 0 V106 L-9 124"
              fill="none"
              strokeWidth="3.5"
              strokeLinecap="round"
              strokeLinejoin="round"
              className="stroke-[#c7c7cc] dark:stroke-[#636366]"
            />
            <rect x="-15" y="120" width="11" height="17" rx="2.5" transform="rotate(27 -9.5 128)" className="fill-[#3a3a3c] dark:fill-[#aeaeb2]" />
            <circle r="11" className="fill-[#e5e5ea] stroke-[#c7c7cc] dark:fill-[#3a3a3c] dark:stroke-[#48484a]" strokeWidth="1" />
            <circle r="4" className="fill-[#aeaeb2] dark:fill-[#8e8e93]" />
          </svg>
        </motion.div>
      </div>
      <div className="text-center">
        <p className="text-[15px] font-semibold tracking-tight text-[#1d1d1f] dark:text-[#f5f5f7]">{title}</p>
        <p className="text-[13px] text-[#86868b]">{artist}</p>
      </div>
    </div>
  );
}

Prompt für KI

Füg ihn in ChatGPT, Claude oder Cursor ein und der Baustein wird mit diesen Einstellungen an dein Projekt angepasst, auch ohne React.

Mehr aus Mikrointeraktionen

Wofür du ihn nutzen kannst

Diese Play-Pause-Animation macht aus einem schlichten Schalter ein Objekt: eine drehende Platte mit eigenem Tonarm. Sie passt zur Website einer Band oder eines DJs, zu einem Plattenladen, zu einem Podcast mit Retro-Charme oder zum Player eines Musik-Portfolios. Die Scheibe braucht einen Moment, bis sie auf Touren kommt, und läuft langsam aus wie ein echter Plattenspieler.

Es ist nur der sichtbare Teil, von allein spielt er keinen Ton ab. Verbinde ihn mit deinem Audioplayer, damit die Platte mit dem Song startet und stoppt. Pass die Farbe des Labels an dein Cover an und trag Song und Artist ein.