v0.1 public beta early access on GitHub
Home / Screens / media-music-player
Screen template

Music Player Screen

Breathing artwork, like button, scrubbable seek, transport with engaged-state pips, volume, a lyrics/share/queue row, and an up-next peek card

pro Expo Go ✓ iosandroid gestures: tap haptics: light · medium · selection
$ npx lumioui add media-music-player
import { MusicPlayerScreen } from "@/screens/media/music-player";
Registry dependencies4
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
media/music-player.tsxsource · yours to edit
import * as React from "react";
import { useEffect, useState } from "react";
import { View, Pressable } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { Icon, type IconName, Image, Slider, Text } from "@lumioui/ui";
import { AnimatedPressable, spring, useHaptic, usePressAnimation, useReducedMotion } from "@lumioui/core";

import { GlowSpot, Mount, World } from "../shared";
export interface Track {
  id: string;
  title: string;
  artist: string;
  album?: string;
  artworkUrl?: string;
  /** Display duration for queue rows — "3:24". */
  duration?: string;
}

export interface MusicPlayerScreenProps {
  track?: Track;
  isPlaying?: boolean;
  /** Position and duration in seconds. */
  position?: number;
  duration?: number;
  /** Controlled liked state. Omit + use defaultLiked for uncontrolled. */
  liked?: boolean;
  defaultLiked?: boolean;
  onLike?: (liked: boolean) => void;
  /** Controlled shuffle / repeat state — tint + dot when active. */
  shuffleOn?: boolean;
  repeatMode?: "off" | "all" | "one";
  onPlayPause?: () => void;
  onNext?: () => void;
  onPrev?: () => void;
  /** Called with a 0–1 fraction when the user scrubs the seek slider. */
  onSeek?: (fraction: number) => void;
  onShuffle?: () => void;
  onRepeat?: () => void;
  /** Chevron-down dismiss affordance in the top-left. */
  onClose?: () => void;
  /** Overflow menu in the top-right. */
  onMore?: () => void;
  /** "Up next" queue — renders the Spotify-style peek card under the
   *  utility row. The first entry shows; tapping opens the full queue. */
  upNext?: Track[];
  /** Bottom utility row — lyrics, devices, share, queue. */
  onLyrics?: () => void;
  onDevices?: () => void;
  onShare?: () => void;
  onQueue?: () => void;
}

const ART =
  "https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=800&h=800&fit=crop&q=75&auto=format";

const DEFAULT_TRACK: Track = {
  id: "1",
  title: "Midnight Drive",
  artist: "Neon Coast",
  album: "Afterglow",
  artworkUrl: ART,
};

const DEFAULT_UP_NEXT: Track[] = [
  {
    id: "2",
    title: "Golden Hour",
    artist: "Neon Coast",
    album: "Afterglow",
    duration: "3:24",
    artworkUrl: ART,
  },
  {
    id: "3",
    title: "Coastline",
    artist: "Neon Coast",
    album: "Afterglow",
    duration: "4:02",
    artworkUrl: ART,
  },
];

function formatTime(seconds: number) {
  const m = Math.floor(seconds / 60);
  const s = Math.floor(seconds % 60);
  return `${m}:${s.toString().padStart(2, "0")}`;
}

function ControlButton({
  label,
  icon,
  onPress,
  large,
  active,
  /** Filled glyph — transport icons (play/pause/skip) read as solid shapes
   *  in every shipped player. */
  filled,
  /** Tiny corner marker — repeat-one's "1" superscript. */
  marker,
}: {
  label: string;
  icon: IconName;
  onPress?: () => void;
  large?: boolean;
  active?: boolean;
  filled?: boolean;
  marker?: string;
}) {
  const haptics = useHaptic();
  // Stronger squish than the default 0.97 — transport controls read better.
  const press = usePressAnimation({ pressedScale: 0.9 });
  return (
    <AnimatedPressable
      onPressIn={press.onPressIn}
      onPressOut={press.onPressOut}
      onPress={() => {
        haptics.medium();
        onPress?.();
      }}
      style={press.animatedStyle}
      accessibilityRole="button"
      accessibilityLabel={label}
      accessibilityState={{ selected: active ?? false }}
      hitSlop={8}
      className={`items-center justify-center ${
        large ? "h-16 w-16 rounded-full bg-[#BEF04C]" : "h-12 w-12"
      }`}
    >
      <View>
        <Icon
          name={icon}
          size={large ? "lg" : "md"}
          color={large ? "primaryForeground" : active ? "primary" : "#F5F3F8"}
          filled={filled}
        />
        {marker ? (
          <View className="absolute -right-1.5 -top-1 h-4 w-4 items-center justify-center rounded-full bg-[#BEF04C]">
            {/* Tiny superscript glyph — no text-scale token is this small,
                so the size/leading are intentional arbitrary values. */}
            <Text className="text-[9px] font-bold leading-[11px] text-primary-foreground">
              {marker}
            </Text>
          </View>
        ) : null}
      </View>
      {/* Active pip — Spotify-style dot under engaged modes. */}
      {active ? (
        <View className="mt-1 h-1 w-1 rounded-full bg-[#BEF04C]" accessible={false} />
      ) : null}
    </AnimatedPressable>
  );
}

/** Music player — artwork that breathes with playback, scrubbable seek,
 *  transport with engaged-state pips, volume, and a utility row. */
export function MusicPlayerScreen({
  track = DEFAULT_TRACK,
  isPlaying = false,
  position = 74,
  duration = 213,
  liked,
  defaultLiked = false,
  onLike,
  shuffleOn = false,
  repeatMode = "off",
  onPlayPause,
  onNext,
  onPrev,
  onSeek,
  onShuffle,
  onRepeat,
  onClose,
  onMore,
  upNext = DEFAULT_UP_NEXT,
  onLyrics,
  onDevices,
  onShare,
  onQueue,
}: MusicPlayerScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const reduceMotion = useReducedMotion();
  const [volume, setVolume] = useState(60);
  const [uncontrolledLiked, setUncontrolledLiked] = useState(defaultLiked);
  // Resolve controlled vs uncontrolled during render, never in an effect.
  const isLiked = liked ?? uncontrolledLiked;
  // Local scrub position — the Slider is controlled by `position`, so
  // without this the thumb would snap back while dragging when the host
  // isn't echoing positions yet. Cleared whenever `position` moves.
  const [scrub, setScrub] = useState<number | null>(null);
  useEffect(() => {
    setScrub(null);
  }, [position]);

  const shown = scrub ?? position;

  // Apple Music idiom — artwork shrinks back when paused, breathes to full
  // size while playing. spring.surface: a soft settle, not a bounce.
  const artScale = useSharedValue(isPlaying ? 1 : 0.9);
  useEffect(() => {
    const target = isPlaying ? 1 : 0.9;
    artScale.value = reduceMotion ? target : withSpring(target, spring.surface);
  }, [isPlaying, reduceMotion, artScale]);
  const artStyle = useAnimatedStyle(() => ({
    transform: [{ scale: artScale.value }],
  }));

  const toggleLike = () => {
    const next = !isLiked;
    haptics.light();
    if (liked === undefined) setUncontrolledLiked(next);
    onLike?.(next);
  };

  return (
    <World primary="#BEF04C">
    <View className="flex-1 bg-[#111114] dark:bg-[#111114]">
      <GlowSpot color="#BEF04C" size={300} className="-top-16 right-[-90px] opacity-40" />
      <GlowSpot color="#8B5CF6" size={240} className="bottom-[20%] left-[-100px] opacity-30" />
      {/* Top row — dismiss, playback context, overflow menu */}
      <View className="px-4" style={{ paddingTop: insets.top }}>
        <View className="flex-row items-center">
          <Pressable
            onPress={() => {
              haptics.light();
              onClose?.();
            }}
            accessibilityRole="button"
            accessibilityLabel="Close player"
            hitSlop={8}
            className="h-11 w-11 items-center justify-center"
          >
            <Icon name="chevron-down" size="md" color="#F5F3F8" />
          </Pressable>
          <View className="flex-1 items-center">
            <Text
              variant="muted"
              className="text-xs uppercase tracking-widest text-[#9B94A8]"
              accessibilityRole="header"
            >
              {track.album ? "Playing from album" : "Now Playing"}
            </Text>
            {track.album ? (
              <Text variant="small" numberOfLines={1} className="text-xs text-[#F5F3F8]">
                {track.album}
              </Text>
            ) : null}
            {/* Stream-quality line — players surface the fidelity tier. */}
            <Text variant="small" className="pt-0.5 text-[10px] tracking-wide text-[#9B94A8]/80">
              LOSSLESS · 16-BIT/44.1 KHZ
            </Text>
          </View>
          <Pressable
            onPress={() => {
              haptics.light();
              onMore?.();
            }}
            accessibilityRole="button"
            accessibilityLabel="More options"
            hitSlop={8}
            className="h-11 w-11 items-center justify-center"
          >
            <Icon name="more-vertical" size="md" color="#F5F3F8" />
          </Pressable>
        </View>
      </View>

      {/* Artwork — centered in the space between header and controls,
          breathing with playback state. Deep radius + shadow sell it as a
          physical sleeve (Apple Music / Spotify idiom). Capped so the
          square never overflows the flex space once the queue card is in. */}
      <View className="flex-1 items-center justify-center px-8 py-2">
        <Mount delay={60} className="w-full max-w-[280px]">
          <Animated.View style={artStyle} className="w-full">
            {track.artworkUrl ? (
              <Image
                source={{ uri: track.artworkUrl }}
                rounded="none"
                aspectRatio={1}
                className="w-full rounded-3xl shadow-2xl"
                accessibilityLabel={`Album art for ${track.album ?? track.title}`}
              />
            ) : (
              <View className="aspect-square w-full items-center justify-center rounded-3xl bg-white/10 shadow-2xl">
                <Icon name="music" size="xl" color="mutedForeground" />
              </View>
            )}
          </Animated.View>
        </Mount>
      </View>

      {/* Track meta + seek + transport + volume, pinned above the home indicator */}
      <View
        className="gap-4 px-8 pt-2"
        style={{ paddingBottom: insets.bottom + 8 }}
      >
        <Mount delay={160} className="flex-row items-center gap-3">
          <View className="flex-1 gap-1">
            <Text variant="h3" numberOfLines={1} className="text-[#F5F3F8]">
              {track.title}
            </Text>
            <Text variant="muted" numberOfLines={1} className="text-[#9B94A8]">
              {track.artist}
              {track.album ? ` · ${track.album}` : ""}
            </Text>
          </View>
          <Pressable
            onPress={toggleLike}
            accessibilityRole="button"
            accessibilityLabel={isLiked ? "Remove from liked songs" : "Like this song"}
            accessibilityState={{ selected: isLiked }}
            hitSlop={8}
            className="h-11 w-11 items-center justify-center"
          >
            <Icon
              name="heart"
              size="md"
              color={isLiked ? "primary" : "mutedForeground"}
              filled={isLiked}
            />
          </Pressable>
        </Mount>

        <Mount delay={240}>
          <Slider
            value={shown}
            min={0}
            max={Math.max(duration, 1)}
            step={1}
            onValueChange={(v) => {
              setScrub(v);
              if (duration > 0) onSeek?.(v / duration);
            }}
            accessibilityLabel="Seek"
          />
          <View className="flex-row justify-between">
            <Text variant="muted" className="font-mono text-xs tabular-nums text-[#9B94A8]">
              {formatTime(shown)}
            </Text>
            <Text variant="muted" className="font-mono text-xs tabular-nums text-[#9B94A8]">
              −{formatTime(Math.max(0, duration - shown))}
            </Text>
          </View>
        </Mount>

        {/* Controls — secondary modes flank the transport; engaged modes
            get the accent tint + a pip underneath (Spotify idiom). */}
        <Mount delay={300} className="flex-row items-center justify-between">
          <ControlButton
            label={shuffleOn ? "Shuffle on" : "Shuffle"}
            icon="shuffle"
            onPress={onShuffle}
            active={shuffleOn}
          />
          <ControlButton label="Previous" icon="skip-back" onPress={onPrev} filled />
          <ControlButton
            label={isPlaying ? "Pause" : "Play"}
            icon={isPlaying ? "pause" : "play"}
            onPress={onPlayPause}
            large
            filled
          />
          <ControlButton label="Next" icon="skip-forward" onPress={onNext} filled />
          <ControlButton
            label={
              repeatMode === "one"
                ? "Repeat one"
                : repeatMode === "all"
                  ? "Repeat all"
                  : "Repeat"
            }
            icon="repeat"
            onPress={onRepeat}
            active={repeatMode !== "off"}
            marker={repeatMode === "one" ? "1" : undefined}
          />
        </Mount>

        {/* Volume + utility row — last to land, below the transport. */}
        <Mount delay={360} className="gap-5">
        <View className="flex-row items-center gap-3">
          <Icon name="volume" size="sm" color="#9B94A8" />
          <View className="flex-1">
            <Slider
              value={volume}
              min={0}
              max={100}
              step={5}
              onValueChange={setVolume}
              accessibilityLabel="Volume"
            />
          </View>
          <Icon name="volume-2" size="sm" color="#9B94A8" />
        </View>

        {/* Utility row — lyrics, devices, share, queue (Apple Music's
            bottom bar). */}
        <View className="flex-row items-center justify-between px-2">
          {(
            [
              { label: "Lyrics", icon: "message-circle", onPress: onLyrics },
              { label: "Playback devices", icon: "wifi", onPress: onDevices },
              { label: "Share song", icon: "share", onPress: onShare },
              { label: "Up next queue", icon: "list-music", onPress: onQueue },
            ] as const
          ).map((action) => (
            <Pressable
              key={action.label}
              onPress={() => {
                haptics.light();
                action.onPress?.();
              }}
              accessibilityRole="button"
              accessibilityLabel={action.label}
              hitSlop={8}
              className="h-11 w-11 items-center justify-center"
            >
              <Icon name={action.icon} size="md" color="#9B94A8" />
            </Pressable>
          ))}
        </View>
        </Mount>

        {/* Up next — Spotify's peek card: next track at a glance, tap
            through to the full queue. */}
        {upNext.length > 0 ? (
          <Mount delay={420}>
            <Pressable
              onPress={() => {
                haptics.light();
                onQueue?.();
              }}
              accessibilityRole="button"
              accessibilityLabel={`Up next: ${upNext[0]!.title} by ${upNext[0]!.artist}`}
              className="flex-row items-center gap-3 rounded-3xl bg-white/10 px-3 py-2.5"
            >
              {upNext[0]!.artworkUrl ? (
                <Image
                  source={{ uri: upNext[0]!.artworkUrl }}
                  rounded="none"
                  className="h-10 w-10 rounded-lg"
                  accessibilityLabel={`${upNext[0]!.album ?? upNext[0]!.title} artwork`}
                />
              ) : (
                <View className="h-10 w-10 items-center justify-center rounded-lg bg-white/10">
                  <Icon name="music" size="sm" color="mutedForeground" />
                </View>
              )}
              <View className="flex-1 gap-0.5">
                <Text
                  variant="muted"
                  className="text-[10px] font-semibold uppercase tracking-widest text-[#9B94A8]"
                >
                  Up next{upNext.length > 1 ? ` · ${upNext.length}` : ""}
                </Text>
                <Text variant="small" numberOfLines={1} className="font-medium text-[#F5F3F8]">
                  {upNext[0]!.title}
                  <Text variant="muted" className="font-normal text-[#9B94A8]">
                    {"  ·  "}
                    {upNext[0]!.artist}
                  </Text>
                </Text>
              </View>
              {upNext[0]!.duration ? (
                <Text variant="muted" className="text-xs tabular-nums text-[#9B94A8]">
                  {upNext[0]!.duration}
                </Text>
              ) : null}
            </Pressable>
          </Mount>
        ) : null}
      </View>
    </View>
    </World>
  );
}
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Music Player Screen" screen in React Native using LumioUI.

Goal: Breathing artwork, like button, scrubbable seek, transport with engaged-state pips, volume, a lyrics/share/queue row, and an up-next peek card Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add media-music-player
```

Or pull just the components: `npx lumioui add icon image slider text`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×9

### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×2

### `slider` — Draggable track with haptic ticks and precise thumb.
- `<Slider>` ×2

### `text` — Themed type scale from caption to display.
- `<Text>` ×12 — Tiny superscript glyph — no text-scale token is this small, so the size/leading are intentional arbitrary values.

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/media/music-player.tsx` exporting MusicPlayerScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { useEffect, useState } from "react";
import { View, Pressable } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { Icon, type IconName, Image, Slider, Text } from "@lumioui/ui";
import { AnimatedPressable, spring, useHaptic, usePressAnimation, useReducedMotion } from "@lumioui/core";

import { GlowSpot, Mount, World } from "../shared";
export interface Track {
  id: string;
  title: string;
  artist: string;
  album?: string;
  artworkUrl?: string;
  /** Display duration for queue rows — "3:24". */
  duration?: string;
}

export interface MusicPlayerScreenProps {
  track?: Track;
  isPlaying?: boolean;
  /** Position and duration in seconds. */
  position?: number;
  duration?: number;
  /** Controlled liked state. Omit + use defaultLiked for uncontrolled. */
  liked?: boolean;
  defaultLiked?: boolean;
  onLike?: (liked: boolean) => void;
  /** Controlled shuffle / repeat state — tint + dot when active. */
  shuffleOn?: boolean;
  repeatMode?: "off" | "all" | "one";
  onPlayPause?: () => void;
  onNext?: () => void;
  onPrev?: () => void;
  /** Called with a 0–1 fraction when the user scrubs the seek slider. */
  onSeek?: (fraction: number) => void;
  onShuffle?: () => void;
  onRepeat?: () => void;
  /** Chevron-down dismiss affordance in the top-left. */
  onClose?: () => void;
  /** Overflow menu in the top-right. */
  onMore?: () => void;
  /** "Up next" queue — renders the Spotify-style peek card under the
   *  utility row. The first entry shows; tapping opens the full queue. */
  upNext?: Track[];
  /** Bottom utility row — lyrics, devices, share, queue. */
  onLyrics?: () => void;
  onDevices?: () => void;
  onShare?: () => void;
  onQueue?: () => void;
}

const ART =
  "https://images.unsplash.com/photo-1541701494587-cb58502866ab?w=800&h=800&fit=crop&q=75&auto=format";

const DEFAULT_TRACK: Track = {
  id: "1",
  title: "Midnight Drive",
  artist: "Neon Coast",
  album: "Afterglow",
  artworkUrl: ART,
};

const DEFAULT_UP_NEXT: Track[] = [
  {
    id: "2",
    title: "Golden Hour",
    artist: "Neon Coast",
    album: "Afterglow",
    duration: "3:24",
    artworkUrl: ART,
  },
  {
    id: "3",
    title: "Coastline",
    artist: "Neon Coast",
    album: "Afterglow",
    duration: "4:02",
    artworkUrl: ART,
  },
];

function formatTime(seconds: number) {
  const m = Math.floor(seconds / 60);
  const s = Math.floor(seconds % 60);
  return `${m}:${s.toString().padStart(2, "0")}`;
}

function ControlButton({
  label,
  icon,
  onPress,
  large,
  active,
  /** Filled glyph — transport icons (play/pause/skip) read as solid shapes
   *  in every shipped player. */
  filled,
  /** Tiny corner marker — repeat-one's "1" superscript. */
  marker,
}: {
  label: string;
  icon: IconName;
  onPress?: () => void;
  large?: boolean;
  active?: boolean;
  filled?: boolean;
  marker?: string;
}) {
  const haptics = useHaptic();
  // Stronger squish than the default 0.97 — transport controls read better.
  const press = usePressAnimation({ pressedScale: 0.9 });
  return (
    <AnimatedPressable
      onPressIn={press.onPressIn}
      onPressOut={press.onPressOut}
      onPress={() => {
        haptics.medium();
        onPress?.();
      }}
      style={press.animatedStyle}
      accessibilityRole="button"
      accessibilityLabel={label}
      accessibilityState={{ selected: active ?? false }}
      hitSlop={8}
      className={`items-center justify-center ${
        large ? "h-16 w-16 rounded-full bg-[#BEF04C]" : "h-12 w-12"
      }`}
    >
      <View>
        <Icon
          name={icon}
          size={large ? "lg" : "md"}
          color={large ? "primaryForeground" : active ? "primary" : "#F5F3F8"}
          filled={filled}
        />
        {marker ? (
          <View className="absolute -right-1.5 -top-1 h-4 w-4 items-center justify-center rounded-full bg-[#BEF04C]">
            {/* Tiny superscript glyph — no text-scale token is this small,
                so the size/leading are intentional arbitrary values. */}
            <Text className="text-[9px] font-bold leading-[11px] text-primary-foreground">
              {marker}
            </Text>
          </View>
        ) : null}
      </View>
      {/* Active pip — Spotify-style dot under engaged modes. */}
      {active ? (
        <View className="mt-1 h-1 w-1 rounded-full bg-[#BEF04C]" accessible={false} />
      ) : null}
    </AnimatedPressable>
  );
}

/** Music player — artwork that breathes with playback, scrubbable seek,
 *  transport with engaged-state pips, volume, and a utility row. */
export function MusicPlayerScreen({
  track = DEFAULT_TRACK,
  isPlaying = false,
  position = 74,
  duration = 213,
  liked,
  defaultLiked = false,
  onLike,
  shuffleOn = false,
  repeatMode = "off",
  onPlayPause,
  onNext,
  onPrev,
  onSeek,
  onShuffle,
  onRepeat,
  onClose,
  onMore,
  upNext = DEFAULT_UP_NEXT,
  onLyrics,
  onDevices,
  onShare,
  onQueue,
}: MusicPlayerScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const reduceMotion = useReducedMotion();
  const [volume, setVolume] = useState(60);
  const [uncontrolledLiked, setUncontrolledLiked] = useState(defaultLiked);
  // Resolve controlled vs uncontrolled during render, never in an effect.
  const isLiked = liked ?? uncontrolledLiked;
  // Local scrub position — the Slider is controlled by `position`, so
  // without this the thumb would snap back while dragging when the host
  // isn't echoing positions yet. Cleared whenever `position` moves.
  const [scrub, setScrub] = useState<number | null>(null);
  useEffect(() => {
    setScrub(null);
  }, [position]);

  const shown = scrub ?? position;

  // Apple Music idiom — artwork shrinks back when paused, breathes to full
  // size while playing. spring.surface: a soft settle, not a bounce.
  const artScale = useSharedValue(isPlaying ? 1 : 0.9);
  useEffect(() => {
    const target = isPlaying ? 1 : 0.9;
    artScale.value = reduceMotion ? target : withSpring(target, spring.surface);
  }, [isPlaying, reduceMotion, artScale]);
  const artStyle = useAnimatedStyle(() => ({
    transform: [{ scale: artScale.value }],
  }));

  const toggleLike = () => {
    const next = !isLiked;
    haptics.light();
    if (liked === undefined) setUncontrolledLiked(next);
    onLike?.(next);
  };

  return (
    <World primary="#BEF04C">
    <View className="flex-1 bg-[#111114] dark:bg-[#111114]">
      <GlowSpot color="#BEF04C" size={300} className="-top-16 right-[-90px] opacity-40" />
      <GlowSpot color="#8B5CF6" size={240} className="bottom-[20%] left-[-100px] opacity-30" />
      {/* Top row — dismiss, playback context, overflow menu */}
      <View className="px-4" style={{ paddingTop: insets.top }}>
        <View className="flex-row items-center">
          <Pressable
            onPress={() => {
              haptics.light();
              onClose?.();
            }}
            accessibilityRole="button"
            accessibilityLabel="Close player"
            hitSlop={8}
            className="h-11 w-11 items-center justify-center"
          >
            <Icon name="chevron-down" size="md" color="#F5F3F8" />
          </Pressable>
          <View className="flex-1 items-center">
            <Text
              variant="muted"
              className="text-xs uppercase tracking-widest text-[#9B94A8]"
              accessibilityRole="header"
            >
              {track.album ? "Playing from album" : "Now Playing"}
            </Text>
            {track.album ? (
              <Text variant="small" numberOfLines={1} className="text-xs text-[#F5F3F8]">
                {track.album}
              </Text>
            ) : null}
            {/* Stream-quality line — players surface the fidelity tier. */}
            <Text variant="small" className="pt-0.5 text-[10px] tracking-wide text-[#9B94A8]/80">
              LOSSLESS · 16-BIT/44.1 KHZ
            </Text>
          </View>
          <Pressable
            onPress={() => {
              haptics.light();
              onMore?.();
            }}
            accessibilityRole="button"
            accessibilityLabel="More options"
            hitSlop={8}
            className="h-11 w-11 items-center justify-center"
          >
            <Icon name="more-vertical" size="md" color="#F5F3F8" />
          </Pressable>
        </View>
      </View>

      {/* Artwork — centered in the space between header and controls,
          breathing with playback state. Deep radius + shadow sell it as a
          physical sleeve (Apple Music / Spotify idiom). Capped so the
          square never overflows the flex space once the queue card is in. */}
      <View className="flex-1 items-center justify-center px-8 py-2">
        <Mount delay={60} className="w-full max-w-[280px]">
          <Animated.View style={artStyle} className="w-full">
            {track.artworkUrl ? (
              <Image
                source={{ uri: track.artworkUrl }}
                rounded="none"
                aspectRatio={1}
                className="w-full rounded-3xl shadow-2xl"
                accessibilityLabel={`Album art for ${track.album ?? track.title}`}
              />
            ) : (
              <View className="aspect-square w-full items-center justify-center rounded-3xl bg-white/10 shadow-2xl">
                <Icon name="music" size="xl" color="mutedForeground" />
              </View>
            )}
          </Animated.View>
        </Mount>
      </View>

      {/* Track meta + seek + transport + volume, pinned above the home indicator */}
      <View
        className="gap-4 px-8 pt-2"
        style={{ paddingBottom: insets.bottom + 8 }}
      >
        <Mount delay={160} className="flex-row items-center gap-3">
          <View className="flex-1 gap-1">
            <Text variant="h3" numberOfLines={1} className="text-[#F5F3F8]">
              {track.title}
            </Text>
            <Text variant="muted" numberOfLines={1} className="text-[#9B94A8]">
              {track.artist}
              {track.album ? ` · ${track.album}` : ""}
            </Text>
          </View>
          <Pressable
            onPress={toggleLike}
            accessibilityRole="button"
            accessibilityLabel={isLiked ? "Remove from liked songs" : "Like this song"}
            accessibilityState={{ selected: isLiked }}
            hitSlop={8}
            className="h-11 w-11 items-center justify-center"
          >
            <Icon
              name="heart"
              size="md"
              color={isLiked ? "primary" : "mutedForeground"}
              filled={isLiked}
            />
          </Pressable>
        </Mount>

        <Mount delay={240}>
          <Slider
            value={shown}
            min={0}
            max={Math.max(duration, 1)}
            step={1}
            onValueChange={(v) => {
              setScrub(v);
              if (duration > 0) onSeek?.(v / duration);
            }}
            accessibilityLabel="Seek"
          />
          <View className="flex-row justify-between">
            <Text variant="muted" className="font-mono text-xs tabular-nums text-[#9B94A8]">
              {formatTime(shown)}
            </Text>
            <Text variant="muted" className="font-mono text-xs tabular-nums text-[#9B94A8]">
              −{formatTime(Math.max(0, duration - shown))}
            </Text>
          </View>
        </Mount>

        {/* Controls — secondary modes flank the transport; engaged modes
            get the accent tint + a pip underneath (Spotify idiom). */}
        <Mount delay={300} className="flex-row items-center justify-between">
          <ControlButton
            label={shuffleOn ? "Shuffle on" : "Shuffle"}
            icon="shuffle"
            onPress={onShuffle}
            active={shuffleOn}
          />
          <ControlButton label="Previous" icon="skip-back" onPress={onPrev} filled />
          <ControlButton
            label={isPlaying ? "Pause" : "Play"}
            icon={isPlaying ? "pause" : "play"}
            onPress={onPlayPause}
            large
            filled
          />
          <ControlButton label="Next" icon="skip-forward" onPress={onNext} filled />
          <ControlButton
            label={
              repeatMode === "one"
                ? "Repeat one"
                : repeatMode === "all"
                  ? "Repeat all"
                  : "Repeat"
            }
            icon="repeat"
            onPress={onRepeat}
            active={repeatMode !== "off"}
            marker={repeatMode === "one" ? "1" : undefined}
          />
        </Mount>

        {/* Volume + utility row — last to land, below the transport. */}
        <Mount delay={360} className="gap-5">
        <View className="flex-row items-center gap-3">
          <Icon name="volume" size="sm" color="#9B94A8" />
          <View className="flex-1">
            <Slider
              value={volume}
              min={0}
              max={100}
              step={5}
              onValueChange={setVolume}
              accessibilityLabel="Volume"
            />
          </View>
          <Icon name="volume-2" size="sm" color="#9B94A8" />
        </View>

        {/* Utility row — lyrics, devices, share, queue (Apple Music's
            bottom bar). */}
        <View className="flex-row items-center justify-between px-2">
          {(
            [
              { label: "Lyrics", icon: "message-circle", onPress: onLyrics },
              { label: "Playback devices", icon: "wifi", onPress: onDevices },
              { label: "Share song", icon: "share", onPress: onShare },
              { label: "Up next queue", icon: "list-music", onPress: onQueue },
            ] as const
          ).map((action) => (
            <Pressable
              key={action.label}
              onPress={() => {
                haptics.light();
                action.onPress?.();
              }}
              accessibilityRole="button"
              accessibilityLabel={action.label}
              hitSlop={8}
              className="h-11 w-11 items-center justify-center"
            >
              <Icon name={action.icon} size="md" color="#9B94A8" />
            </Pressable>
          ))}
        </View>
        </Mount>

        {/* Up next — Spotify's peek card: next track at a glance, tap
            through to the full queue. */}
        {upNext.length > 0 ? (
          <Mount delay={420}>
            <Pressable
              onPress={() => {
                haptics.light();
                onQueue?.();
              }}
              accessibilityRole="button"
              accessibilityLabel={`Up next: ${upNext[0]!.title} by ${upNext[0]!.artist}`}
              className="flex-row items-center gap-3 rounded-3xl bg-white/10 px-3 py-2.5"
            >
              {upNext[0]!.artworkUrl ? (
                <Image
                  source={{ uri: upNext[0]!.artworkUrl }}
                  rounded="none"
                  className="h-10 w-10 rounded-lg"
                  accessibilityLabel={`${upNext[0]!.album ?? upNext[0]!.title} artwork`}
                />
              ) : (
                <View className="h-10 w-10 items-center justify-center rounded-lg bg-white/10">
                  <Icon name="music" size="sm" color="mutedForeground" />
                </View>
              )}
              <View className="flex-1 gap-0.5">
                <Text
                  variant="muted"
                  className="text-[10px] font-semibold uppercase tracking-widest text-[#9B94A8]"
                >
                  Up next{upNext.length > 1 ? ` · ${upNext.length}` : ""}
                </Text>
                <Text variant="small" numberOfLines={1} className="font-medium text-[#F5F3F8]">
                  {upNext[0]!.title}
                  <Text variant="muted" className="font-normal text-[#9B94A8]">
                    {"  ·  "}
                    {upNext[0]!.artist}
                  </Text>
                </Text>
              </View>
              {upNext[0]!.duration ? (
                <Text variant="muted" className="text-xs tabular-nums text-[#9B94A8]">
                  {upNext[0]!.duration}
                </Text>
              ) : null}
            </Pressable>
          </Mount>
        ) : null}
      </View>
    </View>
    </World>
  );
}
```

Screen: https://lumioui.dev/screens/media-music-player
Pixel 10 · Android
9:41
Live preview — the real screen via Expo Web · open full ↗