{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mobile-tab-bar",
  "type": "registry:ui",
  "title": "Mobile Tab Bar",
  "description": "A bottom navigation bar with a sliding indicator, icon bounce, badge counts and an optional raised primary action.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/mobile-tab-bar.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\ntype Spring = { stiffness: number; damping: number; mass: number };\n\nexport interface MobileTabItem {\n  id: string;\n  label: string;\n  /** Icon (decorative; the label names the tab). */\n  icon: React.ReactNode;\n  /** Optional icon shown while the tab is active, e.g. a filled variant. */\n  activeIcon?: React.ReactNode;\n  /** Unread count; hidden when 0 or undefined, shown as \"99+\" above 99. */\n  badge?: number;\n  /** Renders the tab as a link. Without it the tab is a button. */\n  href?: string;\n}\n\nexport interface MobileTabBarPrimaryAction {\n  /** Accessible name, e.g. \"New post\". */\n  label: string;\n  icon: React.ReactNode;\n  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n}\n\nexport interface MobileTabBarProps extends Omit<React.HTMLAttributes<HTMLElement>, \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\" | \"onAnimationEnd\"> {\n  /** 3–5 destinations. */\n  items: MobileTabItem[];\n  /** Active tab id (controlled). */\n  value?: string;\n  /** Initially active tab id (uncontrolled). Defaults to the first item. */\n  defaultValue?: string;\n  /** Called when a tab is pressed. */\n  onValueChange?: (id: string) => void;\n  /** Active indicator style. */\n  indicator?: \"pill\" | \"blob\" | \"line\";\n  /** Which labels are visible. Hidden labels still name the tab for screen readers. */\n  labels?: \"always\" | \"active\" | \"never\";\n  /** Raised centre action button (not a destination). */\n  primaryAction?: MobileTabBarPrimaryAction;\n  /** Bounce the icon when a tab becomes active. */\n  bounce?: boolean;\n  /** Spring for the sliding indicator. */\n  spring?: Spring;\n  /** Accent for the indicator, badges and the primary action. */\n  accent?: string;\n  /** Pad the bar with env(safe-area-inset-bottom) for home-indicator devices. */\n  safeArea?: boolean;\n  /** Pin the bar to the bottom of the viewport. */\n  fixed?: boolean;\n  /** Accessible name of the nav landmark. */\n  label?: string;\n}\n\nfunction subscribeReducedMotion(cb: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", cb);\n  return () => mq.removeEventListener(\"change\", cb);\n}\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/**\n * A bottom navigation bar for mobile apps. It is a navigation landmark: each tab is a link\n * (with href) or a button, and the active one carries aria-current=\"page\" — the same semantics\n * whether you route between pages or swap views in state. A shared-layout indicator (pill, blob or\n * line) slides between tabs, icons bounce on selection, badges show counts, and an optional raised\n * primary action sits in the middle.\n */\nexport function MobileTabBar({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  indicator = \"pill\",\n  labels = \"always\",\n  primaryAction,\n  bounce = true,\n  spring = { stiffness: 500, damping: 36, mass: 0.8 },\n  accent = \"#FF7448\",\n  safeArea = true,\n  fixed = false,\n  label = \"Main\",\n  className = \"\",\n  style,\n  ...props\n}: MobileTabBarProps) {\n  const uid = React.useId();\n  const reduceMotion = usePrefersReducedMotion();\n  const [internal, setInternal] = React.useState(defaultValue ?? items[0]?.id);\n  const active = value ?? internal;\n\n  const select = (id: string) => {\n    if (value === undefined) setInternal(id);\n    if (id !== active) onValueChange?.(id);\n  };\n\n  const layoutTransition = reduceMotion ? { duration: 0 } : { type: \"spring\" as const, ...spring };\n  const middle = Math.ceil(items.length / 2);\n\n  const renderTab = (item: MobileTabItem) => {\n    const isActive = item.id === active;\n    const showLabel = labels === \"always\" || (labels === \"active\" && isActive);\n    const count = item.badge && item.badge > 0 ? (item.badge > 99 ? \"99+\" : String(item.badge)) : null;\n    const tabClass = `group relative flex h-full w-full flex-col items-center justify-center gap-1 rounded-xl px-1 outline-hidden [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-solid focus-visible:outline-[var(--mtb-accent)] ${isActive ? \"text-zinc-950 dark:text-white\" : \"text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200\"}`;\n\n    const content = (\n      <>\n        {indicator === \"line\" && isActive ? (\n          <motion.span\n            layoutId={`${uid}-indicator`}\n            transition={layoutTransition}\n            aria-hidden=\"true\"\n            className=\"absolute inset-x-[22%] top-0 h-[3px] rounded-b-full bg-[var(--mtb-accent)]\"\n          />\n        ) : null}\n        <span className={`relative grid place-items-center ${indicator === \"line\" ? \"h-7 w-12\" : \"h-8 w-12\"}`}>\n          {indicator !== \"line\" && isActive ? (\n            <motion.span\n              layoutId={`${uid}-indicator`}\n              transition={layoutTransition}\n              aria-hidden=\"true\"\n              className={\n                indicator === \"blob\"\n                  ? \"absolute left-1/2 top-1/2 -ml-[18px] -mt-[18px] size-9 rounded-full bg-[var(--mtb-accent)] shadow-[0_6px_16px_-6px_var(--mtb-accent)]\"\n                  : \"absolute inset-0 rounded-full bg-[color-mix(in_oklab,var(--mtb-accent)_20%,transparent)]\"\n              }\n            />\n          ) : null}\n          <motion.span\n            aria-hidden=\"true\"\n            initial={false}\n            animate={isActive && bounce && !reduceMotion ? { y: [0, -5, 0], scale: [1, 1.14, 1] } : { y: 0, scale: 1 }}\n            transition={{ duration: 0.42, ease: [0.34, 1.56, 0.64, 1] }}\n            className={`relative grid size-6 place-items-center [&_svg]:size-[22px] ${indicator === \"blob\" && isActive ? \"text-zinc-950\" : \"\"}`}\n          >\n            {isActive && item.activeIcon ? item.activeIcon : item.icon}\n          </motion.span>\n          <AnimatePresence initial={false}>\n            {count ? (\n              <motion.span\n                key={count}\n                aria-hidden=\"true\"\n                initial={reduceMotion ? { opacity: 0 } : { scale: 0.4, opacity: 0 }}\n                animate={{ scale: 1, opacity: 1 }}\n                exit={reduceMotion ? { opacity: 0 } : { scale: 0.4, opacity: 0 }}\n                transition={reduceMotion ? { duration: 0.1 } : { type: \"spring\", stiffness: 600, damping: 22 }}\n                className=\"absolute -top-0.5 left-[calc(50%+6px)] grid h-4 min-w-4 place-items-center rounded-full bg-red-500 px-1 text-[10px] font-semibold leading-none text-white ring-2 ring-white tabular-nums dark:ring-[#16191c]\"\n              >\n                {count}\n              </motion.span>\n            ) : null}\n          </AnimatePresence>\n        </span>\n        <span\n          className={`max-w-full truncate text-[11px] leading-none tracking-tight transition-[opacity,translate] duration-200 ${isActive ? \"font-semibold\" : \"font-medium\"} ${showLabel ? \"opacity-100\" : labels === \"active\" ? \"translate-y-1 opacity-0\" : \"sr-only\"}`}\n        >\n          {item.label}\n          {count ? <span className=\"sr-only\">, {count} new</span> : null}\n        </span>\n      </>\n    );\n\n    return (\n      <li key={item.id} className=\"h-full min-w-0\">\n        {item.href ? (\n          <a href={item.href} aria-current={isActive ? \"page\" : undefined} className={tabClass} onClick={() => select(item.id)}>\n            {content}\n          </a>\n        ) : (\n          <button type=\"button\" aria-current={isActive ? \"page\" : undefined} className={tabClass} onClick={() => select(item.id)}>\n            {content}\n          </button>\n        )}\n      </li>\n    );\n  };\n\n  const columns = items.length + (primaryAction ? 1 : 0);\n\n  return (\n    <nav\n      aria-label={label}\n      className={`w-full border-t border-zinc-900/10 bg-white/90 backdrop-blur-md dark:border-white/10 dark:bg-[#16191c]/90 ${fixed ? \"fixed inset-x-0 bottom-0 z-40\" : \"relative\"} ${safeArea ? \"pb-[env(safe-area-inset-bottom)]\" : \"\"} ${className}`}\n      style={{ [\"--mtb-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      <ul className=\"grid h-16 items-stretch px-2\" style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}>\n        {items.slice(0, middle).map(renderTab)}\n        {primaryAction ? (\n          <li className=\"relative flex justify-center\">\n            <motion.button\n              type=\"button\"\n              aria-label={primaryAction.label}\n              title={primaryAction.label}\n              onClick={primaryAction.onClick}\n              whileTap={reduceMotion ? undefined : { scale: 0.9 }}\n              transition={{ type: \"spring\", stiffness: 600, damping: 24 }}\n              className=\"absolute -top-5 grid size-14 place-items-center rounded-full bg-[var(--mtb-accent)] text-zinc-950 shadow-[0_10px_24px_-8px_var(--mtb-accent)] ring-[5px] ring-white outline-hidden focus-visible:outline-2 focus-visible:outline-offset-[7px] focus-visible:outline-solid focus-visible:outline-[var(--mtb-accent)] dark:ring-[#16191c] [&_svg]:size-6\"\n            >\n              <span aria-hidden=\"true\" className=\"grid place-items-center\">\n                {primaryAction.icon}\n              </span>\n            </motion.button>\n          </li>\n        ) : null}\n        {items.slice(middle).map(renderTab)}\n      </ul>\n    </nav>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation"
  ],
  "docs": "Documentation: https://wancraft.my/components/mobile-tab-bar"
}
