An interactive iOS-inspired Dynamic Island component with expandable states, smooth animations, and contextual content display.
"use client"
import { Button } from "@/components/ui/button"
import { useTimerStore } from "@/zustand"
import { motion, AnimatePresence } from "motion/react"
import { Bell, BellOff, Pause, Play, XIcon, Music, Timer, Mic, Share, Monitor, Circle } from "lucide-react"
import Link from "next/link"
import { useEffect, useMemo, useState } from "react"
type DynamicIslandState = "idle" | "ring" | "timer" | "recording" | "music" | "airdrop" | "screen-recording"
export default function DynamicIsland() {
const [currentState, setCurrentState] = useState<DynamicIslandState>("idle")
const [isSilent, setIsSilent] = useState(false)
const [recordingBars, setRecordingBars] = useState<number[]>(() => Array(20).fill(5))
const [musicBars, setMusicBars] = useState<number[]>(() => Array(6).fill(5))
const [screenRecordingSeconds, setScreenRecordingSeconds] = useState(0)
const [screenRecordingStopped, setScreenRecordingStopped] = useState(false)
const { time, isRunning, start, pause, reset } = useTimerStore()
// Ring animation effect
useEffect(() => {
if (currentState === "ring") {
const interval = setInterval(() => {
setIsSilent((prev) => !prev)
}, 3000)
return () => clearInterval(interval)
}
}, [currentState])
useEffect(() => {
if (currentState !== "timer") {
reset()
pause()
}
}, [currentState, reset, pause])
// Recording bars animation
useEffect(() => {
if (currentState === "recording") {
start()
const interval = setInterval(() => {
setRecordingBars((bars) => bars.map(() => Math.floor(Math.random() * 10) + 3))
}, 200)
return () => clearInterval(interval)
}
}, [currentState, start])
// Music bars animation
useEffect(() => {
if (currentState === "music") {
let interval: NodeJS.Timeout
if (isRunning) {
interval = setInterval(() => {
setMusicBars((prev) => prev.map(() => Math.floor(Math.random() * 14) + 4))
}, 200)
}
return () => clearInterval(interval)
}
}, [currentState, isRunning])
// Screen recording timer
useEffect(() => {
if (currentState === "screen-recording" && !screenRecordingStopped) {
setScreenRecordingSeconds(0)
setScreenRecordingStopped(false)
const timer = setInterval(() => {
setScreenRecordingSeconds((prev) => (prev < 11 ? prev + 1 : prev))
}, 1000)
return () => clearInterval(timer)
}
}, [currentState, screenRecordingStopped])
// Reset states when switching
useEffect(() => {
if (currentState === "timer") {
// Timer keeps its state
} else if (currentState === "screen-recording") {
setScreenRecordingSeconds(0)
setScreenRecordingStopped(false)
}
}, [currentState])
const formatTime = (t: number) => {
const minutes = String(Math.floor(t / 60)).padStart(1, "0")
const seconds = String(t % 60).padStart(2, "0")
return `${minutes}:${seconds}`
}
const dimensions = useMemo(() => {
switch (currentState) {
case "idle":
return { width: 96, height: 28 }
case "ring":
return { width: isSilent ? 140 : 120, height: 28 }
case "timer":
return { width: 284, height: 64 }
case "recording":
return { width: 200, height: 28 }
case "music":
return { width: 320, height: 280 }
case "airdrop":
return { width: 284, height: 180 }
case "screen-recording":
return { width: 288, height: 64 }
default:
return { width: 96, height: 28 }
}
}, [currentState, isSilent])
const renderContent = () => {
switch (currentState) {
case "idle":
return null
case "ring":
return (
<AnimatePresence mode="wait" initial={false}>
{!isSilent ? (
<motion.div
key="ring"
className="flex items-center justify-between w-full px-3 text-white"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.12, ease: [0.23, 1, 0.32, 1] }}
>
<Bell className="w-4 h-4" />
<span className="text-sm font-medium">Ring</span>
</motion.div>
) : (
<motion.div
key="silent"
className="flex items-center justify-between w-full px-3"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.12, ease: [0.23, 1, 0.32, 1] }}
>
<motion.div
className="flex items-center justify-center bg-orange-600 rounded-full h-5 w-5"
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
transition={{ type: "spring", duration: 0.4, bounce: 0.25 }}
>
<BellOff className="w-3 h-3 text-white" />
</motion.div>
<span className="text-sm text-orange-500 font-medium">Silent</span>
</motion.div>
)}
</AnimatePresence>
)
case "timer":
const timeString = formatTime(time)
return (
<div className="flex items-center justify-between w-full gap-6 py-2 pl-2.5 pr-5">
<motion.div className="text-white flex items-center justify-center gap-2 px-1">
<Button
onClick={isRunning ? pause : start}
className="flex justify-center items-center text-xl cursor-pointer bg-[#694608] hover:bg-[#694608] rounded-full h-10 w-10"
>
{isRunning ? <Pause className="w-4 h-4 text-[#FDB000]" /> : <Play className="w-4 h-4 text-[#FDB000]" />}
</Button>
<Button
onClick={reset}
className="flex justify-center items-center text-xl bg-neutral-700 hover:bg-accent cursor-pointer text-white rounded-full h-10 w-10"
>
<XIcon className="h-4 w-4 text-4xl font-bold" />
</Button>
</motion.div>
<div className="flex items-center gap-3 text-white px-1">
<div className="text-sm font-medium flex items-end justify-end">Timer</div>
<div className="flex text-3xl font-semibold leading-none">
{timeString.split("").map((char, i) => (
<div key={i} className="relative w-[18px] flex justify-center items-center">
<AnimatePresence mode="wait">
<motion.span
key={char + i}
initial={{ y: 14, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -14, opacity: 0 }}
transition={{ duration: 0.2, ease: [0.77, 0, 0.175, 1] }}
className="absolute"
>
{char}
</motion.span>
</AnimatePresence>
</div>
))}
</div>
</div>
</div>
)
case "recording":
return (
<div className="flex items-center justify-between w-full px-2 gap-2">
<div className="flex items-center gap-[2px] h-full">
{recordingBars.map((height, i) => (
<motion.div
key={i}
animate={{ height: `${height}px` }}
transition={{ type: "spring", stiffness: 300, damping: 15, mass: 0.5 }}
className="w-[4px] bg-[#FD4F30] rounded-sm"
/>
))}
</div>
<div className="text-[#FD4F30] text-sm font-mono">{formatTime(time)}</div>
</div>
)
case "music":
const totalTime = 120
const progress = (time / totalTime) * 100
const remainingTime = totalTime - time
const handlePlayPause = () => {
if (time >= totalTime) {
reset()
setTimeout(start, 100)
} else if (isRunning) {
pause()
} else {
start()
}
}
return (
<div className="w-full h-full px-5 py-4 flex flex-col justify-between">
{/* Top Section: Album + Title + Bars */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-linear-to-b from-orange-400 to-pink-600 flex items-center justify-center shadow-md">
<div className="w-9 h-9 rounded-lg bg-linear-to-b from-orange-300 to-pink-500" />
</div>
<div>
<div className="text-white font-semibold text-lg">Glow</div>
<div className="text-neutral-400 text-sm">Echo</div>
</div>
</div>
<div className="flex items-end gap-[3px] h-7">
{musicBars.map((h, i) => (
<motion.div
key={i}
className="w-[3px] bg-blue-400 rounded-full"
animate={{ height: isRunning ? h : 8 }}
transition={{ type: "spring", stiffness: 280, damping: 14, mass: 0.4 }}
/>
))}
</div>
</div>
{/* Progress Bar + Time */}
<div className="space-y-2">
<div className="flex justify-between text-white/80 text-xs">
<span>{formatTime(time)}</span>
<span>-{formatTime(remainingTime)}</span>
</div>
<div className="w-full h-1.5 bg-neutral-700 rounded-full overflow-hidden">
<motion.div
className="h-full bg-white rounded-full"
style={{ width: `${progress}%` }}
transition={{ duration: 0.1 }}
/>
</div>
</div>
{/* Controls (fixed height row so icons don't affect layout) */}
<div className="flex items-center justify-center gap-8 h-16">
{/* Reset */}
<motion.button
whileHover={{ scale: 1.1 }}
className="text-white rounded-full hover:bg-white/10 transition h-12 w-12 flex items-center justify-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
className="w-7 h-7 fill-current"
style={{ transform: "scaleX(-1)" }}
>
<path d="M7.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696z"></path>
<path d="M15.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C8.713 12.69 8 12.345 8 11.692V4.308c0-.653.713-.998 1.233-.696z"></path>
</svg>
</motion.button>
{/* Play / Pause */}
<motion.button
whileHover={{ scale: 1.1 }}
onClick={handlePlayPause}
className="text-white rounded-full backdrop-blur-md shadow-md h-14 w-14 flex items-center justify-center"
>
{isRunning ? (
<svg viewBox="0 0 10 13" className="w-7 h-7 fill-current text-neutral-200">
<path d="M1.03906 12.7266H2.82031C3.5 12.7266 3.85938 12.3672 3.85938 11.6797V1.03906C3.85938 0.328125 3.5 0 2.82031 0H1.03906C0.359375 0 0 0.359375 0 1.03906V11.6797C0 12.3672 0.359375 12.7266 1.03906 12.7266ZM6.71875 12.7266H8.49219C9.17969 12.7266 9.53125 12.3672 9.53125 11.6797V1.03906C9.53125 0.328125 9.17969 0 8.49219 0H6.71875C6.03125 0 5.67188 0.359375 5.67188 1.03906V11.6797C5.67188 12.3672 6.03125 12.7266 6.71875 12.7266Z"></path>
</svg>
) : (
<svg viewBox="0 0 12 14" className="w-8 h-8 fill-current text-neutral-200">
<path d="M0.9375 13.2422C1.25 13.2422 1.51562 13.1172 1.82812 12.9375L10.9375 7.67188C11.5859 7.28906 11.8125 7.03906 11.8125 6.625C11.8125 6.21094 11.5859 5.96094 10.9375 5.58594L1.82812 0.3125C1.51562 0.132812 1.25 0.015625 0.9375 0.015625C0.359375 0.015625 0 0.453125 0 1.13281V12.1172C0 12.7969 0.359375 13.2422 0.9375 13.2422Z"></path>
</svg>
)}
</motion.button>
{/* Pause (second skip) */}
<motion.button
whileHover={{ scale: 1.1 }}
className="text-white rounded-full hover:bg-white/10 transition h-12 w-12 flex items-center justify-center"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" className="w-7 h-7 fill-current">
<path d="M7.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696z"></path>
<path d="M15.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C8.713 12.69 8 12.345 8 11.692V4.308c0-.653.713-.998 1.233-.696z"></path>
</svg>
</motion.button>
</div>
</div>
)
case "airdrop":
return (
<div className="flex w-full flex-col gap-2 p-4">
<div className="flex w-full items-center justify-between">
<div className="flex w-2/3 flex-col items-start gap-2">
<div className="relative flex h-10 w-10 items-center justify-center">
<svg
width="598"
height="525"
viewBox="2 25 598 525"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-12 text-[#159BF4]"
>
<path
d="M245.089 398.614C238.706 404.993 228.716 406.384 221.532 400.924C203.536 387.245 189.405 368.982 180.699 347.924C170.034 322.127 168.094 293.551 175.175 266.549C182.256 239.547 197.97 215.6 219.923 198.357C241.876 181.114 268.864 171.52 296.775 171.038C324.686 170.555 351.989 179.21 374.525 195.684C397.061 212.158 413.594 235.547 421.605 262.288C429.615 289.029 428.664 317.655 418.897 343.806C411.012 364.916 397.731 383.478 380.5 397.709C373.258 403.691 362.781 402.418 356.139 395.776C346.376 386.014 349.309 369.308 359.107 359.581C366.113 352.625 371.91 344.457 376.18 335.396C384.498 317.745 386.515 297.776 381.894 278.817C377.274 259.859 366.296 243.057 350.79 231.211C335.284 219.365 316.186 213.192 296.68 213.719C277.174 214.247 258.438 221.444 243.595 234.111C228.752 246.777 218.698 264.149 215.109 283.329C211.521 302.509 214.615 322.34 223.875 339.516C228.413 347.934 234.298 355.465 241.238 361.845C251.62 371.389 255.064 388.646 245.089 398.614Z"
fill="currentColor"
></path>
<path
d="M256.333 299C256.333 310.316 260.829 321.168 268.83 329.17C276.832 337.171 287.684 341.667 299 341.667C310.316 341.667 321.168 337.171 329.17 329.17C337.171 321.168 341.667 310.316 341.667 299C341.667 287.684 337.171 276.832 329.17 268.83C321.168 260.829 310.316 256.333 299 256.333C287.684 256.333 276.832 260.829 268.83 268.83C260.829 276.832 256.333 287.684 256.333 299Z"
fill="currentColor"
></path>
</svg>
<img
src="https://skiper-ui.com/mac/user.png"
alt="user"
width={24}
height={24}
className="absolute -right-1 bottom-0 rounded-full"
/>
</div>
<div className="text-white">
<h1 className="text-sm font-medium">AirDrop</h1>
<p className="mr-2 text-xs leading-4">
<Link className="text-blue-500" href={"https://x.com/ZingadePiyush"}>
Piyush
</Link>{" "}
would like to share UI components with you all.
</p>
</div>
</div>
<div className="h-24 w-24 overflow-hidden rounded-2xl">
<img
src="https://skiper-ui.com/mac/japan.webp"
alt="preview"
width={96}
height={96}
className="size-full object-cover"
/>
</div>
</div>
<div className="flex w-full gap-2 font-medium mt-3">
<motion.button
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="w-1/2 rounded-full cursor-pointer bg-neutral-700 py-1.5 text-white"
>
Decline
</motion.button>
<motion.button
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="w-1/2 rounded-full cursor-pointer bg-[#012B59] py-1 text-[#159BF4]"
>
Accept
</motion.button>
</div>
</div>
)
case "screen-recording":
const showPreview = screenRecordingSeconds >= 11 || screenRecordingStopped
return (
<div className="flex w-full items-center justify-between gap-2 p-4">
<div className="gap-2 space-y-1 text-white">
<div className="flex items-center gap-1">
{!showPreview && (
<motion.div
className="size-3 rounded-full bg-red-500"
animate={{ opacity: [1, 0.3, 1] }}
transition={{ duration: 1, repeat: Number.POSITIVE_INFINITY }}
/>
)}
<motion.p
className="text-red-500 text-sm"
animate={!showPreview ? { opacity: [1, 0.4, 1] } : {}}
transition={{ duration: 0.5, repeat: Number.POSITIVE_INFINITY }}
>
{`0:${screenRecordingSeconds.toString().padStart(2, "0")}`}
</motion.p>
</div>
<div className="w-30 relative flex h-5 items-center justify-center">
<motion.h1
key={showPreview ? "saved" : "recording"}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="absolute w-full text-sm text-white"
>
{showPreview ? "Recording Saved" : "Recording..."}
</motion.h1>
</div>
</div>
<div className="flex justify-end">
<div className="relative flex size-10 items-center justify-center">
{!showPreview && (
<>
<motion.svg
className="absolute inset-0 text-white"
viewBox="0 0 50 50"
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Number.POSITIVE_INFINITY, ease: "linear" }}
>
<circle
strokeLinecap="round"
strokeWidth="3"
stroke="currentColor"
fill="transparent"
r="23.5"
cx="25"
cy="25"
pathLength="1"
strokeDasharray="1px 1px"
opacity="0.5"
/>
</motion.svg>
<motion.div
className="z-10 size-3.5 rounded-sm bg-red-500"
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1.2, repeat: Number.POSITIVE_INFINITY }}
/>
</>
)}
{showPreview && (
<motion.div
className="absolute inset-0 size-12 overflow-hidden rounded-full"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", duration: 0.5, bounce: 0.3 }}
>
<img
alt="preview"
src="https://skiper-ui.com/mac/japan.webp"
className="object-cover w-full h-full"
/>
</motion.div>
)}
{!showPreview && (
<div onClick={() => setScreenRecordingStopped(true)} className="absolute inset-0 cursor-pointer" />
)}
</div>
</div>
</div>
)
default:
return null
}
}
const buttonConfigs = [
{ state: "idle" as const, icon: Circle, label: "Idle" },
{ state: "ring" as const, icon: Bell, label: "Ring" },
{ state: "timer" as const, icon: Timer, label: "Timer" },
{ state: "recording" as const, icon: Mic, label: "Recording" },
{ state: "music" as const, icon: Music, label: "Music" },
{ state: "airdrop" as const, icon: Share, label: "AirDrop" },
{ state: "screen-recording" as const, icon: Monitor, label: "Screen" },
]
return (
<div className="flex flex-col items-center gap-8 p-8">
{/* Dynamic Island */}
<div className="flex items-center justify-center" role="region" aria-label="Dynamic Island" style={{ height: "300px" }}>
<motion.div
layoutId="dynamic-island"
className="bg-neutral-950 rounded-2xl flex items-center justify-center overflow-hidden will-change-transform"
animate={{
width: dimensions.width,
height: dimensions.height,
}}
transition={{
type: "spring",
duration: 0.5,
bounce: 0.18,
}}
>
<AnimatePresence mode="wait">
<motion.div
key={currentState}
initial={{ opacity: 0, scale: 0.92 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}
className="w-full h-full flex items-center justify-center"
>
{renderContent()}
</motion.div>
</AnimatePresence>
</motion.div>
</div>
{/* Control Buttons */}
<div className="flex flex-wrap gap-3 justify-center max-w-md">
{buttonConfigs.map(({ state, icon: Icon, label }) => (
<motion.button
key={state}
onClick={() => setCurrentState(state)}
className={`flex items-center gap-2 px-4 py-2 rounded-full transition-colors ${currentState === state ? "bg-blue-600 text-white" : "bg-neutral-700 text-neutral-300 hover:bg-neutral-600"
}`}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
transition={{ type: "spring", duration: 0.3, bounce: 0.15 }}
>
<Icon className="w-4 h-4" />
<span className="text-sm font-medium">{label}</span>
</motion.button>
))}
</div>
</div>
)
}