{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A FloatingNavbar component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  motion,\n  useMotionValueEvent,\n  useReducedMotion,\n  useScroll,\n} from \"motion/react\";\nimport { type ReactNode, type RefObject, useId, useState } from \"react\";\n\nexport type FloatingNavbarItem = {\n  href?: string;\n  icon?: ReactNode;\n  id: string;\n  label: string;\n  onSelect?: () => void;\n};\n\nexport type FloatingNavbarProps = {\n  actions?: ReactNode;\n  activeId?: string;\n  blur?: boolean;\n  className?: string;\n  containerRef?: RefObject<HTMLElement | null>;\n  hideOnScroll?: boolean;\n  items: FloatingNavbarItem[];\n  logo?: ReactNode;\n  onActiveChange?: (id: string) => void;\n  position?: \"top\" | \"bottom\";\n  scrollThreshold?: number;\n};\n\nconst DEFAULT_SCROLL_THRESHOLD = 24;\nconst SHRINK_SCROLL_DISTANCE = 120;\nconst HIDE_DISTANCE = 16;\nconst NAV_SPRING = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst HIDE_TRANSITION = {\n  duration: 0.25,\n  ease: [0.645, 0.045, 0.355, 1] as [number, number, number, number],\n};\n\nexport default function FloatingNavbar({\n  items,\n  activeId: controlledActiveId,\n  onActiveChange,\n  logo,\n  actions,\n  hideOnScroll = true,\n  scrollThreshold = DEFAULT_SCROLL_THRESHOLD,\n  blur = true,\n  position = \"top\",\n  containerRef,\n  className,\n}: FloatingNavbarProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const generatedId = useId();\n  const pillLayoutId = `floating-navbar-pill-${generatedId}`;\n\n  const [internalActiveId, setInternalActiveId] = useState(items[0]?.id ?? \"\");\n  const [isHidden, setIsHidden] = useState(false);\n  const [isShrunk, setIsShrunk] = useState(false);\n\n  const isControlled = controlledActiveId !== undefined;\n  const activeId = isControlled ? controlledActiveId : internalActiveId;\n\n  const { scrollY } = useScroll(\n    containerRef\n      ? { container: containerRef as RefObject<HTMLElement> }\n      : undefined\n  );\n\n  useMotionValueEvent(scrollY, \"change\", (latest) => {\n    const previous = scrollY.getPrevious() ?? latest;\n    const delta = latest - previous;\n\n    setIsShrunk(latest > SHRINK_SCROLL_DISTANCE);\n\n    if (!hideOnScroll || shouldReduceMotion || latest < scrollThreshold) {\n      setIsHidden(false);\n      return;\n    }\n\n    if (delta > 0) {\n      setIsHidden(true);\n    } else if (delta < 0) {\n      setIsHidden(false);\n    }\n  });\n\n  const handleSelect = (item: FloatingNavbarItem) => {\n    if (!isControlled) {\n      setInternalActiveId(item.id);\n    }\n    onActiveChange?.(item.id);\n    item.onSelect?.();\n  };\n\n  const hiddenOffset = position === \"top\" ? -HIDE_DISTANCE : HIDE_DISTANCE;\n\n  return (\n    <motion.nav\n      animate={\n        shouldReduceMotion\n          ? { opacity: isHidden ? 0 : 1, y: 0 }\n          : { opacity: isHidden ? 0 : 1, y: isHidden ? hiddenOffset : 0 }\n      }\n      aria-label=\"Primary\"\n      className={cn(\n        \"absolute inset-x-0 z-50 mx-auto flex w-fit items-center gap-1 rounded-full border border-foreground/10 bg-background/90 shadow-lg\",\n        blur && \"backdrop-blur-md\",\n        position === \"top\" ? \"top-4\" : \"bottom-4\",\n        isShrunk ? \"px-2 py-1.5\" : \"px-3 py-2\",\n        className\n      )}\n      initial={false}\n      style={isHidden ? { pointerEvents: \"none\" } : undefined}\n      transition={shouldReduceMotion ? { duration: 0 } : HIDE_TRANSITION}\n    >\n      {logo ? (\n        <div className=\"flex shrink-0 items-center px-1\">{logo}</div>\n      ) : null}\n      <ul className=\"flex items-center gap-1\">\n        {items.map((item) => {\n          const isActive = item.id === activeId;\n          const linkClassName = cn(\n            \"relative z-10 flex items-center gap-1.5 whitespace-nowrap rounded-full px-3 py-1.5 font-medium text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-brand\",\n            isActive\n              ? \"text-foreground\"\n              : \"text-muted-foreground hover:text-foreground\"\n          );\n\n          return (\n            <li className=\"relative\" key={item.id}>\n              {item.href ? (\n                <a\n                  aria-current={isActive ? \"page\" : undefined}\n                  className={linkClassName}\n                  href={item.href}\n                  onClick={() => handleSelect(item)}\n                >\n                  {item.icon ? (\n                    <span aria-hidden=\"true\">{item.icon}</span>\n                  ) : null}\n                  {item.label}\n                </a>\n              ) : (\n                <button\n                  aria-current={isActive ? \"page\" : undefined}\n                  className={linkClassName}\n                  onClick={() => handleSelect(item)}\n                  type=\"button\"\n                >\n                  {item.icon ? (\n                    <span aria-hidden=\"true\">{item.icon}</span>\n                  ) : null}\n                  {item.label}\n                </button>\n              )}\n              {isActive && (\n                <motion.span\n                  className=\"absolute inset-0 rounded-full bg-foreground/10\"\n                  layoutId={pillLayoutId}\n                  transition={shouldReduceMotion ? { duration: 0 } : NAV_SPRING}\n                />\n              )}\n            </li>\n          );\n        })}\n      </ul>\n      {actions ? (\n        <div className=\"flex shrink-0 items-center gap-2 pl-1\">{actions}</div>\n      ) : null}\n    </motion.nav>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/floating-navbar/index.tsx","type":"registry:ui"}],"name":"floating-navbar","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Floating Navbar","type":"registry:ui"}