{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A DynamicIsland component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Bell,\n  CloudLightning,\n  Music2,\n  Pause,\n  Phone,\n  Play,\n  SkipBack,\n  SkipForward,\n  Thermometer,\n  Timer as TimerIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\n\nconst BOUNCE_VARIANTS = {\n  idle: 0.5,\n  \"idle-ring\": 0.5,\n  \"idle-timer\": 0.3,\n  \"ring-idle\": 0.5,\n  \"ring-timer\": 0.35,\n  \"timer-idle\": 0.3,\n  \"timer-ring\": 0.35,\n} as const;\n\nconst DEFAULT_BOUNCE = 0.5;\nconst TIMER_INTERVAL_MS = 1000;\n\n// Idle Component with Weather\nconst DefaultIdle = () => {\n  const [showTemp, setShowTemp] = useState(false);\n\n  return (\n    <motion.div\n      className=\"flex items-center gap-2 px-3 py-2\"\n      layout\n      onHoverEnd={() => setShowTemp(false)}\n      onHoverStart={() => setShowTemp(true)}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.div\n          animate={{ opacity: 1, scale: 1 }}\n          className=\"text-white\"\n          exit={{ opacity: 0, scale: 0.8 }}\n          initial={{ opacity: 0, scale: 0.8 }}\n          key=\"storm\"\n        >\n          <CloudLightning className=\"h-5 w-5 text-white\" />\n        </motion.div>\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {showTemp ? (\n          <motion.div\n            animate={{ opacity: 1, width: \"auto\" }}\n            className=\"flex items-center gap-1 overflow-hidden text-white\"\n            exit={{ opacity: 0, width: 0 }}\n            initial={{ opacity: 0, width: 0 }}\n          >\n            <Thermometer className=\"h-3 w-3\" />\n            <span className=\"pointer-events-none whitespace-nowrap text-white text-xs\">\n              12°C\n            </span>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </motion.div>\n  );\n};\n\n// Ring Component\nconst DefaultRing = () => (\n  <div className=\"flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n    <Phone className=\"h-5 w-5 text-green-500\" />\n    <div className=\"flex-1\">\n      <p className=\"pointer-events-none font-medium text-sm text-white\">\n        Incoming Call\n      </p>\n      <p className=\"pointer-events-none text-white text-xs opacity-70\">\n        Guillermo Rauch\n      </p>\n    </div>\n    <div className=\"h-2 w-2 animate-pulse rounded-full bg-green-500\" />\n  </div>\n);\n\n// Timer Component\nconst DefaultTimer = () => {\n  const [time, setTime] = useState(60);\n\n  useMemo(() => {\n    const timer = setInterval(() => {\n      setTime((t) => (t > 0 ? t - 1 : 0));\n    }, TIMER_INTERVAL_MS);\n    return () => clearInterval(timer);\n  }, []);\n\n  return (\n    <div className=\"flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n      <TimerIcon className=\"h-5 w-5 text-amber-500\" />\n      <div className=\"flex-1\">\n        <p className=\"pointer-events-none font-medium text-sm text-white\">\n          {time}s remaining\n        </p>\n      </div>\n      <div className=\"h-1 w-24 overflow-hidden rounded-full bg-white/20\">\n        <motion.div\n          animate={{ width: \"0%\" }}\n          className=\"h-full bg-amber-500\"\n          initial={{ width: \"100%\" }}\n          transition={{ duration: time, ease: \"linear\" }}\n        />\n      </div>\n    </div>\n  );\n};\n\n// Notification Component\nconst Notification = () => (\n  <div className=\"flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n    <Bell className=\"h-5 w-5 text-yellow-400\" />\n    <div className=\"flex-1\">\n      <p className=\"pointer-events-none font-medium text-sm text-white\">\n        New Message\n      </p>\n      <p className=\"pointer-events-none text-white text-xs opacity-70\">\n        You have a new notification!\n      </p>\n    </div>\n    <span className=\"rounded-full bg-yellow-400/40 px-2 py-0.5 text-xs text-yellow-500\">\n      1\n    </span>\n  </div>\n);\n\n// Music Player Component\nconst MusicPlayer = () => {\n  const [playing, setPlaying] = useState(true);\n  return (\n    <div className=\"flex w-72 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n      <Music2 className=\"h-5 w-5 text-pink-500\" />\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"pointer-events-none truncate font-medium text-sm text-white\">\n          Lofi Chill Beats\n        </p>\n        <p className=\"pointer-events-none truncate text-white text-xs opacity-70\">\n          DJ Smooth\n        </p>\n      </div>\n      <button\n        className=\"rounded-full p-1 hover:bg-white/30\"\n        onClick={() => setPlaying(false)}\n        type=\"button\"\n      >\n        <SkipBack className=\"h-4 w-4 text-white\" />\n      </button>\n      <button\n        className=\"rounded-full p-1 hover:bg-white/30\"\n        onClick={() => setPlaying((p) => !p)}\n        type=\"button\"\n      >\n        {playing ? (\n          <Pause className=\"h-4 w-4 text-white\" />\n        ) : (\n          <Play className=\"h-4 w-4 text-white\" />\n        )}\n      </button>\n      <button\n        className=\"rounded-full p-1 hover:bg-white/30\"\n        onClick={() => setPlaying(true)}\n        type=\"button\"\n      >\n        <SkipForward className=\"h-4 w-4 text-white\" />\n      </button>\n    </div>\n  );\n};\n\ntype View = \"idle\" | \"ring\" | \"timer\" | \"notification\" | \"music\";\n\nexport interface DynamicIslandProps {\n  className?: string;\n  idleContent?: ReactNode;\n  onViewChange?: (view: View) => void;\n  ringContent?: ReactNode;\n  timerContent?: ReactNode;\n  view?: View;\n}\n\nexport default function DynamicIsland({\n  view: controlledView,\n  onViewChange,\n  idleContent,\n  ringContent,\n  timerContent,\n  className = \"\",\n}: DynamicIslandProps) {\n  const [internalView, setInternalView] = useState<View>(\"idle\");\n  const [variantKey, setVariantKey] = useState<string>(\"idle\");\n  const shouldReduceMotion = useReducedMotion();\n\n  const view = controlledView ?? internalView;\n\n  const content = useMemo(() => {\n    switch (view) {\n      case \"ring\":\n        return ringContent ?? <DefaultRing />;\n      case \"timer\":\n        return timerContent ?? <DefaultTimer />;\n      case \"notification\":\n        return <Notification />;\n      case \"music\":\n        return <MusicPlayer />;\n      default:\n        return idleContent ?? <DefaultIdle />;\n    }\n  }, [view, idleContent, ringContent, timerContent]);\n\n  const handleViewChange = (newView: View) => {\n    if (view === newView) {\n      return;\n    }\n    setVariantKey(`${view}-${newView}`);\n    if (onViewChange) {\n      onViewChange(newView);\n    } else {\n      setInternalView(newView);\n    }\n  };\n\n  return (\n    <div className={`h-[200px] ${className}`}>\n      <div className=\"relative flex h-full w-full flex-col justify-center\">\n        <motion.div\n          className=\"mx-auto w-fit min-w-[100px] overflow-hidden rounded-full bg-black\"\n          layout\n          style={{ borderRadius: 32 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  bounce:\n                    BOUNCE_VARIANTS[\n                      variantKey as keyof typeof BOUNCE_VARIANTS\n                    ] ?? DEFAULT_BOUNCE,\n                  duration: 0.25,\n                  type: \"spring\" as const,\n                }\n          }\n        >\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : {\n                    filter: \"blur(0px)\",\n                    opacity: 1,\n                    originX: 0.5,\n                    originY: 0.5,\n                    scale: 1,\n                    transition: { delay: 0.05 },\n                  }\n            }\n            initial={{\n              filter: \"blur(5px)\",\n              opacity: 0,\n              originX: 0.5,\n              originY: 0.5,\n              scale: 0.9,\n            }}\n            key={view}\n            transition={{\n              bounce:\n                BOUNCE_VARIANTS[variantKey as keyof typeof BOUNCE_VARIANTS] ??\n                DEFAULT_BOUNCE,\n              type: \"spring\" as const,\n            }}\n          >\n            {content}\n          </motion.div>\n        </motion.div>\n\n        <div className=\"absolute bottom-2 left-1/2 z-10 flex -translate-x-1/2 justify-center gap-1 rounded-full border bg-background p-1\">\n          {[\n            { icon: <CloudLightning className=\"size-3\" />, key: \"idle\" },\n            { icon: <Phone className=\"size-3\" />, key: \"ring\" },\n            { icon: <TimerIcon className=\"size-3\" />, key: \"timer\" },\n            { icon: <Bell className=\"size-3\" />, key: \"notification\" },\n            { icon: <Music2 className=\"size-3\" />, key: \"music\" },\n          ].map(({ key, icon }) => (\n            <button\n              aria-label={key}\n              className=\"flex size-8 cursor-pointer items-center justify-center rounded-full border bg-primary px-2\"\n              key={key}\n              onClick={() => {\n                if (view !== key) {\n                  setVariantKey(`${view}-${key}`);\n                  handleViewChange(key as View);\n                }\n              }}\n              type=\"button\"\n            >\n              {icon}\n            </button>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/dynamic-island/index.tsx","type":"registry:ui"}],"name":"dynamic-island","registryDependencies":[],"title":"Dynamic Island","type":"registry:ui"}