{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morphing-expandable-menu",
  "type": "registry:ui",
  "title": "Morphing Expandable Menu",
  "description": "A spring-animated pill that morphs into a dock, then expands into search, music player, timer, or note panels.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ruixenui/morphing-expandable-menu.tsx",
      "content": "\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport {\n  Search,\n  Music2,\n  Timer,\n  Pen,\n  X,\n  Play,\n  Pause,\n  SkipForward,\n  SkipBack,\n  RotateCcw,\n  Plus,\n} from \"lucide-react\";\n\ntype Mode = \"pill\" | \"dock\" | \"search\" | \"player\" | \"timer\" | \"note\";\n\nconst spring = {\n  type: \"spring\" as const,\n  damping: 28,\n  stiffness: 380,\n  mass: 0.7,\n};\n\nconst dockActions: { id: Mode; icon: typeof Search }[] = [\n  { id: \"search\", icon: Search },\n  { id: \"player\", icon: Music2 },\n  { id: \"timer\", icon: Timer },\n  { id: \"note\", icon: Pen },\n];\n\nconst dims: Record<Mode, [number, number, number]> = {\n  pill: [44, 44, 50],\n  dock: [200, 44, 22],\n  search: [280, 218, 18],\n  player: [280, 172, 18],\n  timer: [220, 180, 18],\n  note: [256, 182, 18],\n};\n\nexport function MorphingExpandableMenu() {\n  const [mode, setMode] = useState<Mode>(\"pill\");\n  const [hoveredDock, setHoveredDock] = useState<string | null>(null);\n  const ref = useRef<HTMLDivElement>(null);\n\n  const [playing, setPlaying] = useState(false);\n  const [progress] = useState(35);\n  const [timeLeft, setTimeLeft] = useState(25 * 60);\n  const [running, setRunning] = useState(false);\n  const [noteText, setNoteText] = useState(\"\");\n  const [query, setQuery] = useState(\"\");\n\n  useEffect(() => {\n    function onOutside(e: MouseEvent) {\n      if (ref.current && !ref.current.contains(e.target as Node)) {\n        setMode(\"pill\");\n        setQuery(\"\");\n      }\n    }\n    function onKey(e: KeyboardEvent) {\n      if (e.key !== \"Escape\") return;\n      if (mode === \"pill\") return;\n      setMode(mode === \"dock\" ? \"pill\" : \"dock\");\n    }\n    document.addEventListener(\"mousedown\", onOutside);\n    document.addEventListener(\"keydown\", onKey);\n    return () => {\n      document.removeEventListener(\"mousedown\", onOutside);\n      document.removeEventListener(\"keydown\", onKey);\n    };\n  }, [mode]);\n\n  useEffect(() => {\n    if (!running) return;\n    const id = setInterval(() => setTimeLeft((t) => Math.max(0, t - 1)), 1000);\n    return () => clearInterval(id);\n  }, [running]);\n\n  useEffect(() => {\n    if (mode !== \"dock\") setHoveredDock(null);\n  }, [mode]);\n\n  const fmt = (s: number) => {\n    const m = Math.floor(s / 60);\n    const sec = s % 60;\n    return `${String(m).padStart(2, \"0\")}:${String(sec).padStart(2, \"0\")}`;\n  };\n\n  const goBack = () => setMode(\"dock\");\n  const [w, h, r] = dims[mode];\n  const isDockOrPill = mode === \"pill\" || mode === \"dock\";\n\n  return (\n    <div ref={ref} className=\"relative h-11 w-11\">\n      <motion.div\n        className=\"absolute right-0 top-0 overflow-hidden border border-border/60 bg-popover shadow-xl shadow-black/8 dark:border-border/40 dark:shadow-black/25\"\n        animate={{ width: w, height: h, borderRadius: r }}\n        transition={spring}\n      >\n        {/* ── dock / pill bar ──────────────────────────── */}\n        <AnimatePresence>\n          {isDockOrPill && (\n            <motion.div\n              key=\"bar\"\n              className=\"absolute right-0 top-0 flex h-11 items-center\"\n              style={{ width: dims.dock[0] }}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.1 }}\n            >\n              {dockActions.map((item, i) => (\n                <motion.button\n                  key={item.id}\n                  onClick={() => setMode(item.id)}\n                  onMouseEnter={() => setHoveredDock(item.id)}\n                  onMouseLeave={() => setHoveredDock(null)}\n                  animate={{\n                    width: mode === \"dock\" ? 36 : 0,\n                    marginLeft: mode === \"dock\" ? 3 : 0,\n                    opacity: mode === \"dock\" ? 1 : 0,\n                  }}\n                  transition={{\n                    ...spring,\n                    delay: mode === \"dock\" ? (3 - i) * 0.03 : 0,\n                  }}\n                  style={{\n                    height: 36,\n                    pointerEvents: mode === \"dock\" ? \"auto\" : \"none\",\n                  }}\n                  className=\"relative flex shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl text-muted-foreground transition-colors duration-150 hover:text-foreground\"\n                >\n                  {hoveredDock === item.id && (\n                    <motion.div\n                      layoutId=\"dockGlow\"\n                      className=\"absolute inset-0 rounded-xl bg-foreground/[0.05] dark:bg-foreground/[0.08]\"\n                      transition={spring}\n                    />\n                  )}\n                  <item.icon className=\"relative z-10 size-[17px]\" />\n                </motion.button>\n              ))}\n\n              <motion.button\n                onClick={() => setMode(mode === \"pill\" ? \"dock\" : \"pill\")}\n                className=\"ml-auto flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center\"\n                whileTap={{ scale: 0.85 }}\n              >\n                <motion.div\n                  animate={{ rotate: mode === \"dock\" ? 45 : 0 }}\n                  transition={{\n                    type: \"spring\",\n                    damping: 20,\n                    stiffness: 300,\n                    mass: 0.5,\n                  }}\n                >\n                  <Plus className=\"size-5 text-foreground/80\" />\n                </motion.div>\n              </motion.button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        {/* ── panels ───────────────────────────────────── */}\n        <AnimatePresence mode=\"wait\" initial={false}>\n          {/* search */}\n          {mode === \"search\" && (\n            <motion.div\n              key=\"search\"\n              className=\"flex h-full flex-col p-3\"\n              initial={{ opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(4px)\" }}\n              transition={{ duration: 0.15 }}\n            >\n              <div className=\"flex items-center gap-2 border-b border-border/40 pb-2.5\">\n                <Search className=\"size-4 shrink-0 text-muted-foreground/60\" />\n                <input\n                  autoFocus\n                  value={query}\n                  onChange={(e) => setQuery(e.target.value)}\n                  placeholder=\"Search...\"\n                  className=\"flex-1 bg-transparent text-[13px] outline-none placeholder:text-muted-foreground/50\"\n                />\n                <button\n                  onClick={goBack}\n                  className=\"text-muted-foreground/60 hover:text-foreground\"\n                >\n                  <X className=\"size-3.5\" />\n                </button>\n              </div>\n              <div className=\"scrollbar-hide mt-1 min-h-0 flex-1 overflow-y-auto\">\n                <p className=\"mt-1.5 px-1 text-[10px] font-medium uppercase tracking-widest text-muted-foreground/50\">\n                  Recent\n                </p>\n                <div className=\"mt-1 space-y-0.5\">\n                  {[\n                    \"Project settings\",\n                    \"API documentation\",\n                    \"Team members\",\n                    \"Billing\",\n                  ].map((item, i) => (\n                    <motion.div\n                      key={item}\n                      initial={{ opacity: 0, x: -6 }}\n                      animate={{ opacity: 1, x: 0 }}\n                      transition={{ delay: 0.06 + i * 0.025, ...spring }}\n                      className=\"cursor-pointer rounded-lg px-2 py-[6px] text-[13px] text-muted-foreground transition-colors duration-150 hover:bg-foreground/[0.04] hover:text-foreground\"\n                    >\n                      {item}\n                    </motion.div>\n                  ))}\n                </div>\n              </div>\n            </motion.div>\n          )}\n\n          {/* player */}\n          {mode === \"player\" && (\n            <motion.div\n              key=\"player\"\n              className=\"flex h-full flex-col p-4\"\n              initial={{ opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(4px)\" }}\n              transition={{ duration: 0.15 }}\n            >\n              <div className=\"flex items-start justify-between\">\n                <div className=\"flex items-center gap-3\">\n                  <div className=\"size-10 shrink-0 rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-500\" />\n                  <div>\n                    <p className=\"text-[13px] font-medium text-foreground\">\n                      Midnight City\n                    </p>\n                    <p className=\"text-[11px] text-muted-foreground/70\">\n                      M83 — Hurry Up, We&apos;re Dreaming\n                    </p>\n                  </div>\n                </div>\n                <button\n                  onClick={goBack}\n                  className=\"text-muted-foreground/50 hover:text-foreground\"\n                >\n                  <X className=\"size-3.5\" />\n                </button>\n              </div>\n\n              <div className=\"mt-4\">\n                <div className=\"h-[3px] w-full overflow-hidden rounded-full bg-foreground/8\">\n                  <motion.div\n                    className=\"h-full rounded-full bg-foreground/40\"\n                    initial={{ width: \"0%\" }}\n                    animate={{ width: `${progress}%` }}\n                    transition={{ duration: 0.5, ease: \"easeOut\" }}\n                  />\n                </div>\n                <div className=\"mt-1 flex justify-between text-[10px] text-muted-foreground/50\">\n                  <span>1:24</span>\n                  <span>4:03</span>\n                </div>\n              </div>\n\n              <div className=\"mt-2 flex items-center justify-center gap-6\">\n                <motion.button\n                  whileTap={{ scale: 0.85 }}\n                  className=\"cursor-pointer text-muted-foreground/60 hover:text-foreground\"\n                >\n                  <SkipBack className=\"size-4\" />\n                </motion.button>\n                <motion.button\n                  whileTap={{ scale: 0.8 }}\n                  onClick={() => setPlaying(!playing)}\n                  className=\"flex size-9 cursor-pointer items-center justify-center rounded-full bg-foreground text-background\"\n                >\n                  {playing ? (\n                    <Pause className=\"size-[15px]\" />\n                  ) : (\n                    <Play className=\"ml-0.5 size-[15px]\" />\n                  )}\n                </motion.button>\n                <motion.button\n                  whileTap={{ scale: 0.85 }}\n                  className=\"cursor-pointer text-muted-foreground/60 hover:text-foreground\"\n                >\n                  <SkipForward className=\"size-4\" />\n                </motion.button>\n              </div>\n            </motion.div>\n          )}\n\n          {/* timer */}\n          {mode === \"timer\" && (\n            <motion.div\n              key=\"timer\"\n              className=\"flex h-full flex-col p-4\"\n              initial={{ opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(4px)\" }}\n              transition={{ duration: 0.15 }}\n            >\n              <div className=\"flex items-center justify-between\">\n                <p className=\"text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/50\">\n                  Focus\n                </p>\n                <button\n                  onClick={goBack}\n                  className=\"text-muted-foreground/50 hover:text-foreground\"\n                >\n                  <X className=\"size-3.5\" />\n                </button>\n              </div>\n\n              <p className=\"mt-4 text-center font-mono text-[40px] font-semibold leading-none tabular-nums text-foreground\">\n                {fmt(timeLeft)}\n              </p>\n\n              <div className=\"mt-5 flex items-center justify-center gap-2\">\n                <motion.button\n                  whileTap={{ scale: 0.92 }}\n                  onClick={() => setRunning(!running)}\n                  className=\"h-8 cursor-pointer rounded-lg bg-foreground px-5 text-xs font-medium text-background\"\n                >\n                  {running ? \"Pause\" : \"Start\"}\n                </motion.button>\n                <motion.button\n                  whileTap={{ scale: 0.88 }}\n                  onClick={() => {\n                    setRunning(false);\n                    setTimeLeft(25 * 60);\n                  }}\n                  className=\"flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-border/50 text-muted-foreground/60 hover:text-foreground\"\n                >\n                  <RotateCcw className=\"size-3.5\" />\n                </motion.button>\n              </div>\n            </motion.div>\n          )}\n\n          {/* note */}\n          {mode === \"note\" && (\n            <motion.div\n              key=\"note\"\n              className=\"flex h-full flex-col p-3\"\n              initial={{ opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(4px)\" }}\n              transition={{ duration: 0.15 }}\n            >\n              <div className=\"mb-2 flex items-center justify-between\">\n                <p className=\"text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/50\">\n                  Quick Note\n                </p>\n                <button\n                  onClick={goBack}\n                  className=\"text-muted-foreground/50 hover:text-foreground\"\n                >\n                  <X className=\"size-3.5\" />\n                </button>\n              </div>\n              <textarea\n                autoFocus\n                value={noteText}\n                onChange={(e) => setNoteText(e.target.value)}\n                placeholder=\"Type something...\"\n                className=\"flex-1 resize-none rounded-lg bg-foreground/[0.03] p-2.5 text-[13px] outline-none transition-colors placeholder:text-muted-foreground/40 focus:bg-foreground/[0.05]\"\n              />\n              <div className=\"mt-2 flex items-center justify-between\">\n                <span className=\"text-[10px] text-muted-foreground/40\">\n                  {noteText.length} chars\n                </span>\n                <motion.button\n                  whileTap={{ scale: 0.92 }}\n                  className=\"h-7 cursor-pointer rounded-md bg-foreground px-3 text-[11px] font-medium text-background\"\n                >\n                  Save\n                </motion.button>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/morphing-expandable-menu.tsx"
    }
  ]
}