UI Elements

Dynamic Island

An interactive iOS-inspired Dynamic Island component with expandable states, smooth animations, and contextual content display.

dynamic-island.tsx
"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>
  )
}