{
  "name": "dynamic-island",
  "type": "registry:ui",
  "title": "Dynamic Island",
  "description": "Floating dynamic island control bar with section tracking, circular progress indicator, font switcher, and theme color controls.",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "registry/navigation/dynamic-island/dynamic-island.tsx",
      "type": "registry:ui",
      "target": "components/ui/dynamic-island.tsx",
      "content": "import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport { motion, AnimatePresence } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\nexport type IslandPosition =\n  | \"top-left\"\n  | \"top-center\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-center\"\n  | \"bottom-right\"\n  | \"inline\";\n\nexport interface IslandSection {\n  id: string;\n  label: string;\n}\n\nexport interface IslandFont {\n  name: string;\n  value: string;\n}\n\nexport interface IslandTheme {\n  name: string;\n  color: string;\n  color2?: string;\n}\n\nexport interface DynamicIslandProps {\n  position?: IslandPosition;\n  sections?: IslandSection[];\n  defaultSectionLabel?: string;\n  sectionIcon?: React.ReactNode;\n  fonts?: IslandFont[];\n  showFontPicker?: boolean;\n  defaultFontIndex?: number;\n  themes?: IslandTheme[];\n  showThemePicker?: boolean;\n  defaultThemeIndex?: number;\n  storageKey?: string;\n  disablePersistence?: boolean;\n  className?: string;\n  onSectionChange?: (section: IslandSection | null) => void;\n  onFontChange?: (font: IslandFont) => void;\n  onThemeChange?: (theme: IslandTheme) => void;\n}\n\nconst DEFAULT_FONTS: IslandFont[] = [\n  { name: \"Inter\", value: \"Inter, sans-serif\" },\n  { name: \"Geist\", value: \"'Geist', sans-serif\" },\n  { name: \"Mono\", value: \"'Geist Mono', monospace\" },\n];\n\nconst DEFAULT_THEMES: IslandTheme[] = [\n  { name: \"Indigo\", color: \"#6366f1\", color2: \"#818cf8\" },\n  { name: \"Emerald\", color: \"#10b981\", color2: \"#34d399\" },\n  { name: \"Violet\", color: \"#8b5cf6\", color2: \"#a78bfa\" },\n  { name: \"Rose\", color: \"#f43f5e\", color2: \"#fb7185\" },\n];\n\nconst DEFAULT_SECTIONS: IslandSection[] = [\n  { id: \"hero\", label: \"Hero\" },\n  { id: \"features\", label: \"Features\" },\n  { id: \"pricing\", label: \"Pricing\" },\n];\n\nfunction getPositionClasses(position: IslandPosition): string {\n  switch (position) {\n    case \"top-left\": return \"fixed top-4 left-4 z-50\";\n    case \"top-center\": return \"fixed top-4 left-1/2 -translate-x-1/2 z-50\";\n    case \"top-right\": return \"fixed top-4 right-4 z-50\";\n    case \"bottom-left\": return \"fixed bottom-4 left-4 z-50\";\n    case \"bottom-right\": return \"fixed bottom-4 right-4 z-50\";\n    case \"bottom-center\": return \"fixed bottom-4 left-1/2 -translate-x-1/2 z-50\";\n    case \"inline\":\n    default:\n      return \"relative z-30\";\n  }\n}\n\nexport function DynamicIsland({\n  position = \"inline\",\n  sections = DEFAULT_SECTIONS,\n  defaultSectionLabel = \"Overview\",\n  sectionIcon,\n  fonts = DEFAULT_FONTS,\n  showFontPicker = true,\n  defaultFontIndex = 0,\n  themes = DEFAULT_THEMES,\n  showThemePicker = true,\n  defaultThemeIndex = 0,\n  storageKey = \"island\",\n  disablePersistence = true,\n  className = \"\",\n  onSectionChange,\n  onFontChange,\n  onThemeChange,\n}: DynamicIslandProps) {\n  const [activeTab, setActiveTab] = useState<\"font\" | \"theme\" | null>(null);\n  const [currentFont, setCurrentFont] = useState<IslandFont>(fonts[defaultFontIndex] ?? fonts[0]);\n  const [currentTheme, setCurrentTheme] = useState<IslandTheme>(themes[defaultThemeIndex] ?? themes[0]);\n  const [scrollProgress, setScrollProgress] = useState(35);\n  const [currentSection, setCurrentSection] = useState(defaultSectionLabel);\n  const islandRef = useRef<HTMLDivElement>(null);\n\n  const dropDir = position.startsWith(\"top\") ? \"down\" : \"up\";\n  const positionCls = getPositionClasses(position);\n  const dropdownBase = dropDir === \"up\" ? \"absolute bottom-full mb-2.5\" : \"absolute top-full mt-2.5\";\n\n  useEffect(() => {\n    const handler = (e: MouseEvent) => {\n      if (islandRef.current && !islandRef.current.contains(e.target as Node)) {\n        setActiveTab(null);\n      }\n    };\n    document.addEventListener(\"mousedown\", handler);\n    return () => document.removeEventListener(\"mousedown\", handler);\n  }, []);\n\n  const accentColor = currentTheme.color;\n\n  return (\n    <div className={cn(positionCls, className)}>\n      <div\n        ref={islandRef}\n        className=\"relative flex items-center p-1.5 rounded-full border border-zinc-200/90 dark:border-white/10 bg-white/90 dark:bg-zinc-900/90 shadow-xl backdrop-blur-xl select-none\"\n      >\n        {/* Font Picker */}\n        {showFontPicker && fonts.length > 0 && (\n          <div className=\"relative\">\n            <button\n              onClick={() => setActiveTab(activeTab === \"font\" ? null : \"font\")}\n              className={cn(\n                \"px-3.5 py-1.5 rounded-full text-xs font-medium transition-colors cursor-pointer\",\n                activeTab === \"font\"\n                  ? \"bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-white\"\n                  : \"text-zinc-600 dark:text-zinc-300 hover:text-zinc-950 dark:hover:text-white hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60\"\n              )}\n            >\n              {currentFont.name}\n            </button>\n\n            <AnimatePresence>\n              {activeTab === \"font\" && (\n                <motion.div\n                  initial={{ opacity: 0, y: dropDir === \"up\" ? 8 : -8, scale: 0.95 }}\n                  animate={{ opacity: 1, y: 0, scale: 1 }}\n                  exit={{ opacity: 0, y: dropDir === \"up\" ? 8 : -8, scale: 0.95 }}\n                  transition={{ duration: 0.15 }}\n                  className={cn(\n                    dropdownBase,\n                    \"left-0 w-40 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl shadow-2xl p-1.5 flex flex-col z-50\"\n                  )}\n                >\n                  {fonts.map((font) => (\n                    <button\n                      key={font.name}\n                      onClick={() => {\n                        setCurrentFont(font);\n                        setActiveTab(null);\n                        onFontChange?.(font);\n                      }}\n                      className={cn(\n                        \"flex items-center px-3 py-2 rounded-xl text-xs font-medium transition-colors cursor-pointer\",\n                        currentFont.name === font.name\n                          ? \"bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-white\"\n                          : \"text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800/50\"\n                      )}\n                    >\n                      <span style={{ fontFamily: font.value }}>{font.name}</span>\n                    </button>\n                  ))}\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        )}\n\n        {showFontPicker && <div className=\"w-[1px] h-3.5 bg-zinc-200 dark:bg-white/10 mx-1\" />}\n\n        {/* Scroll Progress & Section */}\n        <button\n          onClick={() => {\n            if (typeof window !== \"undefined\") window.scrollTo({ top: 0, behavior: \"smooth\" });\n          }}\n          className=\"flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium text-zinc-600 dark:text-zinc-300 hover:text-zinc-950 dark:hover:text-white transition-colors cursor-pointer\"\n        >\n          <div className=\"relative w-4 h-4 flex items-center justify-center\">\n            {sectionIcon ? (\n              sectionIcon\n            ) : (\n              <svg className=\"w-full h-full -rotate-90\" viewBox=\"0 0 36 36\">\n                <path\n                  strokeWidth=\"3.5\"\n                  stroke=\"currentColor\"\n                  fill=\"none\"\n                  className=\"text-zinc-200 dark:text-white/20\"\n                  d=\"M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831\"\n                />\n                <path\n                  strokeWidth=\"3.5\"\n                  fill=\"none\"\n                  stroke={accentColor}\n                  strokeDasharray={`${scrollProgress}, 100`}\n                  strokeLinecap=\"round\"\n                  d=\"M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831\"\n                />\n              </svg>\n            )}\n          </div>\n          <span className=\"w-16 text-center truncate\">{currentSection}</span>\n        </button>\n\n        {showThemePicker && <div className=\"w-[1px] h-3.5 bg-zinc-200 dark:bg-white/10 mx-1\" />}\n\n        {/* Theme Picker */}\n        {showThemePicker && themes.length > 0 && (\n          <div className=\"relative\">\n            <button\n              onClick={() => setActiveTab(activeTab === \"theme\" ? null : \"theme\")}\n              className={cn(\n                \"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors cursor-pointer\",\n                activeTab === \"theme\"\n                  ? \"bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-white\"\n                  : \"text-zinc-600 dark:text-zinc-300 hover:text-zinc-950 dark:hover:text-white hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60\"\n              )}\n            >\n              <span\n                className=\"w-2.5 h-2.5 rounded-full shrink-0\"\n                style={{ backgroundColor: currentTheme.color }}\n              />\n              <span>{currentTheme.name}</span>\n            </button>\n\n            <AnimatePresence>\n              {activeTab === \"theme\" && (\n                <motion.div\n                  initial={{ opacity: 0, y: dropDir === \"up\" ? 8 : -8, scale: 0.95 }}\n                  animate={{ opacity: 1, y: 0, scale: 1 }}\n                  exit={{ opacity: 0, y: dropDir === \"up\" ? 8 : -8, scale: 0.95 }}\n                  transition={{ duration: 0.15 }}\n                  className={cn(\n                    dropdownBase,\n                    \"right-0 w-36 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl shadow-2xl p-1.5 flex flex-col z-50\"\n                  )}\n                >\n                  {themes.map((theme) => (\n                    <button\n                      key={theme.name}\n                      onClick={() => {\n                        setCurrentTheme(theme);\n                        setActiveTab(null);\n                        onThemeChange?.(theme);\n                      }}\n                      className={cn(\n                        \"flex items-center gap-2 px-2.5 py-1.5 rounded-xl text-xs font-medium transition-colors cursor-pointer\",\n                        currentTheme.name === theme.name\n                          ? \"bg-zinc-100 dark:bg-zinc-800 text-zinc-950 dark:text-white\"\n                          : \"text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800/50\"\n                      )}\n                    >\n                      <span\n                        className=\"w-2.5 h-2.5 rounded-full shrink-0\"\n                        style={{ backgroundColor: theme.color }}\n                      />\n                      <span>{theme.name}</span>\n                    </button>\n                  ))}\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default DynamicIsland;\n"
    }
  ],
  "$schema": "https://www.cooki-ui.design/schema/registry-item.json"
}