{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "bottom-navigation",
  "title": "Bottom Navigation",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/bottom-navigation/BottomNavigation.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Component } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface BottomNavItem {\n    /** Unique value identifying this tab. Used with bind:value. */\n    value: string\n    /** Label shown under the icon. */\n    label: string\n    /** Lucide icon component (from `@lucide/svelte`). */\n    icon: Component\n    /** Optional badge count or text shown on the icon. */\n    badge?: string | number\n    /** Link destination. Selected tabs navigate here: via `onnavigate` when\n     *  provided (e.g. SvelteKit `goto`), otherwise a full page load. */\n    to?: string\n  }\n\n  export interface BottomNavigationProps extends Omit<HTMLAttributes<HTMLElement>, 'onselect'> {\n    /** Tab items. */\n    items: BottomNavItem[]\n    /** Active item value. Bind it (`bind:value`) for two-way updates. */\n    value?: string\n    /** Active item color — a Tailwind text color class. Default 'text-primary'. */\n    activeColor?: string\n    /** Fixed positioning at the viewport bottom. Default true. */\n    fixed?: boolean\n    /** Show a sliding active indicator pill behind the icon. Default true. */\n    showIndicator?: boolean\n    /** Safe-area padding for notched devices (iOS). Default true. */\n    safeArea?: boolean\n    /** Fires with the new value when a tab is selected. */\n    onValueChange?: (value: string) => void\n    /** Fires when a tab is selected. */\n    onselect?: (item: BottomNavItem) => void\n    /** Custom navigation for items with `to` (e.g. SvelteKit `goto`). When\n     *  omitted, `to` falls back to `window.location.assign`. */\n    onnavigate?: (to: string, item: BottomNavItem) => void\n    /** The <nav> element, via `bind:ref`. */\n    ref?: HTMLElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { tick } from 'svelte'\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    items,\n    value = $bindable(''),\n    activeColor = 'text-primary',\n    fixed = true,\n    showIndicator = true,\n    safeArea = true,\n    onValueChange,\n    onselect,\n    onnavigate,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: BottomNavigationProps = $props()\n\n  // Destructured (not rendered): items render from the `items` array, not a\n  // slot — this keeps a stray `children` prop out of the nav's spread attributes.\n  void children\n\n  let indicatorStyle = $state<Record<string, string>>({ opacity: '0' })\n  let firstPosition = true\n\n  function motionSafeTransition() {\n    if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n      return 'none'\n    }\n    return firstPosition\n      ? 'none'\n      : 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1), width 220ms cubic-bezier(0.22, 1, 0.36, 1), height 220ms cubic-bezier(0.22, 1, 0.36, 1)'\n  }\n\n  function updateIndicator() {\n    if (!showIndicator) return\n    const root = ref\n    if (!root) return\n\n    const activeItem = root.querySelector<HTMLElement>('[data-slot=\"bottom-navigation-item\"][data-active]')\n    if (!activeItem) {\n      indicatorStyle = { opacity: '0' }\n      return\n    }\n\n    const iconWrap = activeItem.querySelector<HTMLElement>('[data-slot=\"bottom-navigation-icon\"]') ?? activeItem\n    const rootRect = root.getBoundingClientRect()\n    const iconRect = iconWrap.getBoundingClientRect()\n\n    // Compact pill centered on the icon — fixed height keeps motion light; width tracks icon + pad.\n    const padX = 14\n    const pillH = 32\n    const pillW = Math.max(iconRect.width + padX * 2, 56)\n    const left = iconRect.left - rootRect.left + root.scrollLeft + (iconRect.width - pillW) / 2\n    const top = iconRect.top - rootRect.top + root.scrollTop + (iconRect.height - pillH) / 2\n    const transition = motionSafeTransition()\n\n    indicatorStyle = {\n      width: `${pillW}px`,\n      height: `${pillH}px`,\n      transform: `translate3d(${left}px, ${top}px, 0)`,\n      opacity: '1',\n      transition,\n    }\n    firstPosition = false\n  }\n\n  // Observe layout while the indicator is on; hide it when off.\n  $effect(() => {\n    if (!showIndicator || !ref) {\n      if (!showIndicator) {\n        firstPosition = true\n        indicatorStyle = { opacity: '0' }\n      }\n      return\n    }\n    firstPosition = true\n    const root = ref\n    const ro = new ResizeObserver(() => updateIndicator())\n    ro.observe(root)\n    root.querySelectorAll('[data-slot=\"bottom-navigation-item\"]').forEach((el) => ro.observe(el))\n    updateIndicator()\n    return () => ro.disconnect()\n  })\n\n  // Slide the pill when the active tab changes (DOM must settle first).\n  $effect(() => {\n    void value\n    void tick().then(() => updateIndicator())\n  })\n\n  // Remeasure without sliding when the item set changes (count/layout).\n  $effect(() => {\n    void items.length\n    firstPosition = true\n    void tick().then(() => updateIndicator())\n  })\n\n  function onSelect(item: BottomNavItem) {\n    if (item.to) {\n      if (onnavigate) {\n        onnavigate(item.to, item)\n      } else if (typeof window !== 'undefined') {\n        window.location.assign(item.to)\n      }\n    }\n    value = item.value\n    onValueChange?.(item.value)\n    onselect?.(item)\n  }\n\n  const indicatorStyleAttr = $derived(\n    Object.entries(indicatorStyle)\n      .map(([k, v]) => `${k}: ${v}`)\n      .join('; '),\n  )\n</script>\n\n<nav\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"bottom-navigation\"\n  aria-label=\"Bottom navigation\"\n  data-fixed={fixed ? '' : undefined}\n  class={cn(\n    'border-border bg-background/95 z-50 flex items-stretch justify-around border-t backdrop-blur-sm',\n    // fixed establishes the containing block for the absolute indicator; relative when in-flow\n    fixed ? 'fixed inset-x-0 bottom-0' : 'relative',\n    safeArea && 'pb-[env(safe-area-inset-bottom)]',\n    className,\n  )}\n  {...restProps}\n>\n  <!-- Single sliding pill; transform-only for cheap mobile paint -->\n  {#if showIndicator}\n    <span\n      data-slot=\"bottom-navigation-indicator\"\n      aria-hidden=\"true\"\n      class=\"bg-primary/10 pointer-events-none absolute top-0 left-0 z-0 rounded-full will-change-transform\"\n      style={indicatorStyleAttr}\n    ></span>\n  {/if}\n\n  {#each items as item (item.value)}\n    {@const Icon = item.icon}\n    {@const isActive = value === item.value}\n    <button\n      data-slot=\"bottom-navigation-item\"\n      data-active={isActive ? '' : undefined}\n      aria-current={isActive ? 'page' : undefined}\n      type=\"button\"\n      class={cn(\n        'focus-visible:ring-ring/50 relative z-10 flex min-h-12 flex-1 flex-col items-center justify-center gap-0.5 pt-2 pb-1.5 text-xs transition-colors duration-200 outline-none focus-visible:ring-[3px] motion-reduce:transition-none',\n        isActive ? activeColor : 'text-muted-foreground hover:text-foreground',\n      )}\n      onclick={() => onSelect(item)}\n    >\n      <span data-slot=\"bottom-navigation-icon\" class=\"relative flex items-center justify-center\">\n        <Icon\n          class={cn(\n            'size-5 transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none',\n            isActive ? 'scale-110' : 'scale-100',\n          )}\n        />\n        {#if item.badge !== undefined && item.badge !== ''}\n          <span\n            class=\"bg-destructive text-destructive-foreground absolute -top-1.5 -right-2 flex min-w-4 items-center justify-center rounded-full px-1 text-xs leading-4 font-medium\"\n          >\n            {item.badge}\n          </span>\n        {/if}\n      </span>\n      <span\n        class={cn(\n          'max-w-full truncate px-1 transition-[opacity,font-weight] duration-200 motion-reduce:transition-none',\n          isActive ? 'font-medium' : 'font-normal',\n        )}\n      >\n        {item.label}\n      </span>\n    </button>\n  {/each}\n</nav>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/bottom-navigation/BottomNavigation.svelte"
    },
    {
      "path": "packages/registry-svelte/components/bottom-navigation/index.ts",
      "content": "export { default as BottomNavigation, type BottomNavigationProps, type BottomNavItem } from './BottomNavigation.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/bottom-navigation/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Mobile bottom tab bar with icon + label items. Supports bind:value for the active item, an active color, fixed positioning at the viewport bottom, badges on items, and a `to` prop for router integration (via `onnavigate`, e.g. SvelteKit `goto`).",
  "categories": [
    "navigation"
  ]
}