{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "floating-toolbar",
  "type": "registry:ui",
  "title": "Floating Toolbar",
  "description": "A floating toolbar with animated tab switching, cursor glow, and morphing search input.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/floating-toolbar.tsx",
      "content": "\"use client\";\n\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { useState, useRef, useEffect } from \"react\";\n\n/**\n * Floating Toolbar — Rauno Freiberg craft.\n *\n * One surface. Underline tracks active. Hover follows cursor.\n * Container breathes via layout. Cursor glow under glass.\n * Staggered choreography. Per-role springs.\n */\n\n/* ── Springs — tuned per visual weight ── */\n\nconst spring = {\n  shell: { type: \"spring\" as const, stiffness: 420, damping: 34 },\n  line: { type: \"spring\" as const, stiffness: 500, damping: 32, mass: 0.8 },\n  hover: { type: \"spring\" as const, stiffness: 400, damping: 28 },\n  content: { type: \"spring\" as const, stiffness: 340, damping: 28 },\n  press: { type: \"spring\" as const, stiffness: 600, damping: 32 },\n} as const;\n\n/* ── Types ── */\n\ninterface Tab {\n  id: string;\n  label: string;\n}\n\ninterface FloatingToolbarProps {\n  tabs?: Tab[];\n  onTabChange?: (id: string) => void;\n  onSearch?: (query: string) => void;\n}\n\nconst DEFAULT_TABS: Tab[] = [\n  { id: \"all\", label: \"All\" },\n  { id: \"recent\", label: \"Recent\" },\n  { id: \"starred\", label: \"Starred\" },\n];\n\n/* ── Component ── */\n\nexport function FloatingToolbar({\n  tabs = DEFAULT_TABS,\n  onTabChange,\n  onSearch,\n}: FloatingToolbarProps) {\n  const [active, setActive] = useState(tabs[0]?.id ?? \"\");\n  const [hovered, setHovered] = useState<string | null>(null);\n  const [mode, setMode] = useState<\"browse\" | \"search\">(\"browse\");\n  const [query, setQuery] = useState(\"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  /* Cursor glow tracking */\n  const [glowX, setGlowX] = useState(0);\n  const [glowOn, setGlowOn] = useState(false);\n\n  /* Focus input on search open */\n  useEffect(() => {\n    if (mode === \"search\") {\n      const t = setTimeout(() => inputRef.current?.focus(), 80);\n      return () => clearTimeout(t);\n    }\n  }, [mode]);\n\n  /* Keyboard: ⌘K opens, Esc closes */\n  useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n        e.preventDefault();\n        setMode(\"search\");\n      }\n      if (e.key === \"Escape\" && mode === \"search\") dismiss();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mode]);\n\n  const select = (id: string) => {\n    setActive(id);\n    onTabChange?.(id);\n  };\n\n  const dismiss = () => {\n    setMode(\"browse\");\n    setQuery(\"\");\n    onSearch?.(\"\");\n  };\n\n  const searching = mode === \"search\";\n\n  return (\n    <div className=\"flex items-center justify-center\">\n      {/* One surface — layout-animated shell */}\n      <motion.div\n        layout\n        transition={spring.shell}\n        style={{ borderRadius: 14 }}\n        className=\"relative flex w-fit items-center h-10 bg-neutral-900 border border-white/[0.06] shadow-[inset_0_1px_0_rgba(255,255,255,0.03),0_1px_2px_rgba(0,0,0,0.4),0_2px_8px_rgba(0,0,0,0.2)]\"\n        onMouseMove={(e) => {\n          const r = e.currentTarget.getBoundingClientRect();\n          setGlowX(e.clientX - r.left);\n          if (!glowOn) setGlowOn(true);\n        }}\n        onMouseLeave={() => {\n          setGlowOn(false);\n          setHovered(null);\n        }}\n      >\n        {/* ── Cursor glow — light-under-glass ── */}\n        <div\n          className=\"absolute inset-0 pointer-events-none z-0\"\n          style={{\n            borderRadius: 14,\n            opacity: glowOn ? 1 : 0,\n            transition: \"opacity 0.3s ease\",\n            background: `radial-gradient(120px circle at ${glowX}px 50%, rgba(255,255,255,0.04), transparent)`,\n          }}\n        />\n\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {!searching ? (\n            /* ────────────────────────\n               BROWSE — tabs + search\n            ──────────────────────── */\n            <motion.div\n              key=\"browse\"\n              className=\"relative z-10 flex items-center\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.15 }}\n            >\n              <div className=\"relative flex items-center px-1\">\n                {tabs.map((tab, i) => (\n                  <motion.button\n                    key={tab.id}\n                    onClick={() => select(tab.id)}\n                    onMouseEnter={() => setHovered(tab.id)}\n                    initial={{ opacity: 0, y: 4 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -3 }}\n                    whileTap={{ scale: 0.96 }}\n                    transition={{ ...spring.content, delay: i * 0.03 }}\n                    className={`relative px-3 py-1.5 text-[13px] font-medium tracking-[-0.01em] outline-none cursor-pointer select-none transition-colors duration-200 ${\n                      active === tab.id\n                        ? \"text-white\"\n                        : \"text-neutral-500 hover:text-neutral-300\"\n                    }`}\n                  >\n                    {/* Hover follower — slides between tabs */}\n                    {hovered === tab.id && (\n                      <motion.div\n                        layoutId=\"tab-hover\"\n                        className=\"absolute inset-1 rounded-lg bg-white/[0.04]\"\n                        transition={spring.hover}\n                      />\n                    )}\n\n                    {/* Active underline — inherently centered, no measurement */}\n                    {active === tab.id && (\n                      <motion.div\n                        layoutId=\"tab-active\"\n                        className=\"absolute bottom-0 left-2 right-2 h-[2px] bg-white rounded-full\"\n                        transition={spring.line}\n                      />\n                    )}\n\n                    <span className=\"relative z-10\">{tab.label}</span>\n                  </motion.button>\n                ))}\n              </div>\n\n              {/* Divider */}\n              <div className=\"w-px h-3.5 bg-white/[0.06] mx-0.5 shrink-0\" />\n\n              {/* Search trigger */}\n              <motion.button\n                onClick={() => setMode(\"search\")}\n                whileHover={{ scale: 1.08 }}\n                whileTap={{ scale: 0.88 }}\n                transition={spring.press}\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                className=\"flex items-center justify-center w-7 h-7 mr-1 rounded-lg text-neutral-500 hover:text-neutral-300 hover:bg-white/[0.04] transition-colors duration-200 outline-none cursor-pointer\"\n                aria-label=\"Search\"\n              >\n                <SearchIcon />\n              </motion.button>\n            </motion.div>\n          ) : (\n            /* ────────────────────────\n               SEARCH — input + esc\n            ──────────────────────── */\n            <motion.div\n              key=\"search\"\n              className=\"relative z-10 flex items-center gap-2 px-3\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.15 }}\n            >\n              {/* Icon — static, quiet */}\n              <motion.div\n                initial={{ opacity: 0, scale: 0.7 }}\n                animate={{ opacity: 0.4, scale: 1 }}\n                transition={{ ...spring.content, delay: 0.02 }}\n                className=\"shrink-0 text-neutral-500\"\n              >\n                <SearchIcon />\n              </motion.div>\n\n              {/* Input */}\n              <motion.div\n                className=\"w-[160px]\"\n                initial={{ opacity: 0, x: -4 }}\n                animate={{ opacity: 1, x: 0 }}\n                transition={{ ...spring.content, delay: 0.05 }}\n              >\n                <input\n                  ref={inputRef}\n                  value={query}\n                  onChange={(e) => {\n                    setQuery(e.target.value);\n                    onSearch?.(e.target.value);\n                  }}\n                  placeholder=\"Search…\"\n                  className=\"w-full bg-transparent outline-none text-[13px] font-medium tracking-[-0.01em] text-white placeholder:text-neutral-600 caret-neutral-500\"\n                />\n              </motion.div>\n\n              {/* Esc — whisper-quiet, earns attention on hover */}\n              <motion.button\n                onClick={dismiss}\n                initial={{ opacity: 0, scale: 0.7 }}\n                animate={{ opacity: 0.3, scale: 1 }}\n                whileHover={{ opacity: 0.7 }}\n                whileTap={{ scale: 0.9 }}\n                transition={{ ...spring.content, delay: 0.08 }}\n                className=\"shrink-0 text-[10px] font-medium tracking-[0.04em] text-neutral-500 border border-white/[0.06] rounded-md px-1.5 py-0.5 cursor-pointer outline-none uppercase\"\n              >\n                esc\n              </motion.button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n\n/* ── Icon ── */\n\nfunction SearchIcon() {\n  return (\n    <svg\n      width=\"14\"\n      height=\"14\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <circle cx=\"11\" cy=\"11\" r=\"7\" />\n      <path d=\"m21 21-4.35-4.35\" />\n    </svg>\n  );\n}\n\nexport default FloatingToolbar;\n",
      "type": "registry:ui",
      "target": "components/ruixen/floating-toolbar.tsx"
    }
  ]
}