Breathing artwork, like button, scrubbable seek, transport with engaged-state pips, volume, a lyrics/share/queue row, and an up-next peek card
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>
);
}
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