Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion
TSX
dynamic-island
"use client"

import React, { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"
// Ensure lucide-react is installed for these icons
import { Phone, Zap, Check } from "lucide-react"

export const componentMeta = {
  name: "Dynamic Island",
  description: "An interactive, iOS-inspired dynamic island component that morphs fluidly between immersive states.",
  props: {
    className: { type: "string", description: "Additional CSS classes", default: '""' },
  }
}

type IslandState = "idle" | "music" | "call" | "notification" | "battery"

export default function DynamicIsland({ className }: { className?: string }) {
  const [state, setState] = useState<IslandState>("idle")

  const ISLAND_VARIANTS = {
    idle: { width: 120, height: 40, borderRadius: 20 },
    music: { width: 340, height: 80, borderRadius: 40 },
    call: { width: 320, height: 72, borderRadius: 36 },
    notification: { width: 300, height: 72, borderRadius: 36 },
    battery: { width: 280, height: 50, borderRadius: 25 }
  }

  return (
    <div className={cn("flex flex-col items-center justify-center p-8 gap-12 w-full", className)}>
      {/* The Island */}
      <div className="relative flex justify-center w-full min-h-[120px]">
        <motion.div
          layout
          variants={ISLAND_VARIANTS}
          initial="idle"
          animate={state}
          transition={{ type: "spring", stiffness: 400, damping: 30 }}
          className="bg-black text-white overflow-hidden shadow-2xl relative flex items-center justify-center cursor-pointer"
          onClick={() => setState("idle")}
        >
          <AnimatePresence mode="popLayout">
            
            {/* IDLE STATE */}
            {state === "idle" && (
              <motion.div
                key="idle"
                initial={{ opacity: 0, scale: 0.8 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0 }}
                className="w-full h-full flex items-center justify-center gap-4"
              >
                {/* Camera / Sensor cutouts mimicking the design */}
                <div className="w-[14px] h-[14px] rounded-full bg-[#111111] shadow-[inset_0px_1px_2px_rgba(255,255,255,0.05)]" />
                <div className="w-8 h-[14px] rounded-full bg-[#111111] shadow-[inset_0px_1px_2px_rgba(255,255,255,0.05)]" />
              </motion.div>
            )}

            {/* MUSIC (SPOTIFY) STATE */}
            {state === "music" && (
              <motion.div
                key="music"
                initial={{ opacity: 0, filter: "blur(4px)" }}
                animate={{ opacity: 1, filter: "blur(0px)" }}
                exit={{ opacity: 0, filter: "blur(4px)" }}
                transition={{ duration: 0.2 }}
                className="w-full h-full flex items-center justify-between px-6 gap-4"
              >
                <svg viewBox="0 0 24 24" fill="#1DB954" className="w-12 h-12 flex-shrink-0 drop-shadow-sm">
                  <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4.586 14.424c-.18.295-.573.398-.868.217-2.38-1.455-5.372-1.785-8.9-1.002-.335.074-.664-.136-.738-.47-.074-.335.136-.665.47-.738 3.864-.856 7.155-.48 9.82 1.15.295.18.397.573.216.868zm1.2-2.738c-.226.368-.71.49-1.077.264-2.718-1.67-6.867-2.15-9.62-1.18-.415.147-.866-.07-1.012-.486-.147-.414.07-.865.485-1.012 3.197-1.127 7.807-.59 10.96 1.344.368.225.49.71.264 1.076zm.106-2.88c-3.23-1.92-8.544-2.096-11.616-1.16-.487.148-.99-.126-1.137-.613-.148-.487.125-.99.613-1.137 3.54-1.075 9.42-.88 13.14 1.336.438.26.58.85.32 1.288-.26.437-.85.58-1.288.32z"/>
                </svg>
                <div className="flex-1 flex flex-col justify-center text-left">
                   <span className="text-[15px] font-bold truncate leading-tight tracking-[0.01em]">Spotify</span>
                   <span className="text-[12px] text-zinc-400 truncate mt-0.5">Playing Music...</span>
                </div>
                <div className="flex items-center gap-1.5 h-6">
                   <motion.div animate={{ height: [12, 24, 12] }} transition={{ repeat: Infinity, duration: 0.6 }} className="w-1 bg-[#1DB954] rounded-full" />
                   <motion.div animate={{ height: [16, 8, 16] }} transition={{ repeat: Infinity, duration: 0.5 }} className="w-1 bg-[#1DB954] rounded-full" />
                   <motion.div animate={{ height: [8, 20, 8] }} transition={{ repeat: Infinity, duration: 0.7 }} className="w-1 bg-[#1DB954] rounded-full" />
                </div>
              </motion.div>
            )}

            {/* CALL STATE */}
            {state === "call" && (
              <motion.div
                key="call"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className="w-full h-full flex items-center justify-between px-3"
              >
                <div className="flex items-center gap-3">
                  <div className="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-400 to-purple-600 flex items-center justify-center shadow-inner overflow-hidden relative">
                     {/* Abstract Avatar Graphic */}
                     <div className="absolute bottom-0 w-8 h-5 bg-white/20 rounded-t-full" />
                     <div className="absolute top-2 w-5 h-5 bg-white/20 rounded-full" />
                  </div>
                  <div className="flex flex-col text-left">
                    <span className="text-[15px] font-semibold text-white leading-tight">Mom</span>
                    <span className="text-[12px] text-zinc-400">Incoming call...</span>
                  </div>
                </div>
                <div className="flex gap-2">
                   <motion.button whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="w-11 h-11 rounded-full bg-[#FF3B30] flex items-center justify-center shadow-lg">
                     <Phone size={20} fill="white" className="text-white rotate-[135deg]" />
                   </motion.button>
                   <motion.button 
                     animate={{ 
                       boxShadow: [
                         "0px 0px 0px 0px rgba(52,199,89,0.8)", 
                         "0px 0px 0px 8px rgba(52,199,89,0)",
                       ] 
                     }} 
                     transition={{ repeat: Infinity, duration: 1.5 }}
                     whileHover={{ scale: 1.05 }} 
                     whileTap={{ scale: 0.95 }} 
                     className="w-11 h-11 rounded-full bg-[#34C759] flex items-center justify-center shadow-lg"
                   >
                     <Phone size={20} fill="white" className="text-white" />
                   </motion.button>
                </div>
              </motion.div>
            )}

            {/* NOTIFICATION STATE (APPLE PAY) */}
            {state === "notification" && (
              <motion.div
                key="notification"
                initial={{ opacity: 0, scale: 0.9 }}
                animate={{ opacity: 1, scale: 1 }}
                exit={{ opacity: 0 }}
                className="w-full h-full flex items-center justify-between px-4"
              >
                <div className="flex items-center gap-3">
                  <div className="w-11 h-11 bg-zinc-900 border border-zinc-800 rounded-full flex items-center justify-center shadow-md">
                    <span className="text-2xl text-white"></span>
                  </div>
                  <div className="flex flex-col text-left">
                    <span className="text-[14px] font-bold text-white tracking-wide leading-tight">Apple Pay</span>
                    <span className="text-[12px] text-zinc-300 font-medium">$120.00 Sent</span>
                  </div>
                </div>
                
                <div className="min-w-[32px] min-h-[32px] rounded-full flex items-center justify-center bg-zinc-800">
                  <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", stiffness: 500, damping: 25, delay: 0.2 }}>
                    <Check size={18} strokeWidth={3} className="text-[#34C759]" />
                  </motion.div>
                </div>
              </motion.div>
            )}

            {/* BATTERY STATE */}
            {state === "battery" && (
              <motion.div
                key="battery"
                initial={{ opacity: 0, y: -10 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: 10 }}
                className="w-full h-full flex items-center justify-between px-5"
              >
                <div className="flex items-center gap-2">
                  <Zap size={18} className="text-[#34C759] fill-[#34C759]" />
                  <span className="text-[14px] font-semibold text-white">Charging</span>
                </div>
                
                <div className="flex items-center gap-2">
                  <span className="text-[14px] font-bold text-[#34C759] mr-1">84%</span>
                  {/* Detailed Boxy Battery Graphic */}
                  <div className="relative w-9 h-[18px] rounded-[4px] border border-zinc-500 flex items-center p-[2px]">
                     <motion.div 
                       initial={{ width: "20%" }} 
                       animate={{ width: "84%" }} 
                       transition={{ duration: 1.5, ease: "easeOut", delay: 0.1 }} 
                       className="h-full bg-[#34C759] rounded-[2px]" 
                     />
                     <div className="absolute -right-[3px] top-1/2 -translate-y-1/2 w-[2px] h-[6px] bg-zinc-500 rounded-r-sm" />
                  </div>
                </div>
              </motion.div>
            )}

          </AnimatePresence>
        </motion.div>
      </div>

      {/* Controls */}
      <div className="flex flex-wrap gap-3 justify-center items-center p-4 bg-zinc-100 dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800">
        {(["idle", "music", "call", "notification", "battery"] as const).map((s) => (
          <button
            key={s}
            onClick={() => setState(s)}
            className={cn(
              "px-4 py-2 text-xs font-semibold rounded-lg capitalize transition-colors duration-200",
              state === s 
                ? "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900" 
                : "bg-white text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700 shadow-sm border border-zinc-200 dark:border-zinc-700"
            )}
          >
            {s}
          </button>
        ))}
      </div>
    </div>
  )
}
Save to favoritesCopy to Figma
React

Dynamic Island

An interactive, iOS-inspired dynamic island component that morphs fluidly between immersive states.

Installation

$ npx @cosmoo/oblivion add dynamic-island
Dependenciesnpm install framer-motion lucide-react

Usage Example

tsx
import DynamicIsland from "@/components/oblivion/dynamic-island"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <DynamicIsland />
    </div>
  )
}