{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "bottom-navigation",
  "title": "Bottom Navigation",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/bottom-navigation/bottom-navigation.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  booleanAttribute,\n  inject,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser'\nimport { NgTemplateOutlet } from '@angular/common'\nimport { cn } from '@/lib/utils'\n\nexport interface BottomNavItem {\n  /** Unique value identifying this tab. Used with value / modelValue. */\n  value: string\n  /** Label shown under the icon. */\n  label: string\n  /** Icon: SVG string, template, or text. */\n  icon?: string | TemplateRef<unknown>\n  /** Optional badge count or text shown on the icon. */\n  badge?: string | number\n  /** Link destination (renders an anchor instead of a button). */\n  to?: string\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-bottom-navigation, [ui-bottom-navigation]',\n  standalone: true,\n  imports: [NgTemplateOutlet],\n  host: {\n    '[attr.data-slot]': '\"bottom-navigation\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-fixed]': \"fixed ? '' : null\",\n    role: 'navigation',\n    '[attr.aria-label]': '\"Bottom navigation\"',\n    '[class]': 'hostClass',\n  },\n  template: `\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.width]=\"indicatorStyle['width']\"\n        [style.height]=\"indicatorStyle['height']\"\n        [style.transform]=\"indicatorStyle['transform']\"\n        [style.opacity]=\"indicatorStyle['opacity']\"\n        [style.transition]=\"indicatorStyle['transition']\"\n      ></span>\n    }\n    @for (item of items; track item.value) {\n      @if (item.to) {\n        <a\n          [href]=\"item.to\"\n          data-slot=\"bottom-navigation-item\"\n          [attr.data-active]=\"isActive(item) ? '' : null\"\n          [attr.aria-current]=\"isActive(item) ? 'page' : null\"\n          [class]=\"itemClass(item)\"\n          (click)=\"onSelect(item)\"\n        >\n          <span data-slot=\"bottom-navigation-icon\" class=\"relative flex items-center justify-center\">\n            @if (isSvg(item.icon)) {\n              <span\n                class=\"flex size-5 items-center justify-center transition-transform duration-200 motion-reduce:transition-none [&>svg,&>lucide-icon>svg]:size-5\"\n                [class.scale-110]=\"isActive(item)\"\n                [innerHTML]=\"sanitize(item.icon)\"\n                aria-hidden=\"true\"\n              ></span>\n            } @else if (isTemplate(item.icon)) {\n              <ng-container [ngTemplateOutlet]=\"item.icon\" />\n            } @else {\n              <span\n                class=\"flex size-5 items-center justify-center transition-transform duration-200 motion-reduce:transition-none\"\n                [class.scale-110]=\"isActive(item)\"\n                aria-hidden=\"true\"\n                >{{ item.icon ?? '●' }}</span\n              >\n            }\n            @if (item.badge !== undefined && item.badge !== '') {\n              <span\n                data-slot=\"bottom-navigation-badge\"\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            }\n          </span>\n          <span\n            data-slot=\"bottom-navigation-label\"\n            [class]=\"labelClass(item)\"\n          >\n            {{ item.label }}\n          </span>\n        </a>\n      } @else {\n        <button\n          type=\"button\"\n          data-slot=\"bottom-navigation-item\"\n          [attr.data-active]=\"isActive(item) ? '' : null\"\n          [attr.aria-current]=\"isActive(item) ? 'page' : null\"\n          [class]=\"itemClass(item)\"\n          (click)=\"onSelect(item)\"\n        >\n          <span data-slot=\"bottom-navigation-icon\" class=\"relative flex items-center justify-center\">\n            @if (isSvg(item.icon)) {\n              <span\n                class=\"flex size-5 items-center justify-center transition-transform duration-200 motion-reduce:transition-none [&>svg,&>lucide-icon>svg]:size-5\"\n                [class.scale-110]=\"isActive(item)\"\n                [innerHTML]=\"sanitize(item.icon)\"\n                aria-hidden=\"true\"\n              ></span>\n            } @else if (isTemplate(item.icon)) {\n              <ng-container [ngTemplateOutlet]=\"item.icon\" />\n            } @else {\n              <span\n                class=\"flex size-5 items-center justify-center transition-transform duration-200 motion-reduce:transition-none\"\n                [class.scale-110]=\"isActive(item)\"\n                aria-hidden=\"true\"\n                >{{ item.icon ?? '●' }}</span\n              >\n            }\n            @if (item.badge !== undefined && item.badge !== '') {\n              <span\n                data-slot=\"bottom-navigation-badge\"\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            }\n          </span>\n          <span\n            data-slot=\"bottom-navigation-label\"\n            [class]=\"labelClass(item)\"\n          >\n            {{ item.label }}\n          </span>\n        </button>\n      }\n    }\n  `,\n})\nexport class UiBottomNavigationComponent implements AfterViewInit, OnChanges, OnDestroy {\n  @Input() items: BottomNavItem[] = []\n  @Input() value?: string\n  @Input() defaultValue = ''\n  @Input() modelValue = ''\n  @Input() activeColor = 'text-primary'\n  @Input({ transform: booleanAttribute }) fixed = true\n  @Input({ transform: booleanAttribute }) showIndicator = true\n  @Input({ transform: booleanAttribute }) safeArea = true\n  @Input('class') className?: string\n\n  @Output() valueChange = new EventEmitter<string>()\n  @Output() onValueChange = new EventEmitter<string>()\n  @Output() modelValueChange = new EventEmitter<string>()\n  @Output() select = new EventEmitter<BottomNavItem>()\n\n  private ro: ResizeObserver | null = null\n  private firstPosition = true\n  private prevItemsKey = ''\n  private readonly sanitizer: DomSanitizer | null = null\n  private readonly el: ElementRef<HTMLElement> | null = null\n\n  constructor() {\n    try {\n      this.sanitizer = inject(DomSanitizer, { optional: true })\n    } catch {}\n    try {\n      this.el = inject<ElementRef<HTMLElement>>(ElementRef, { optional: true })\n    } catch {}\n  }\n\n  private sanitizedCache = new Map<string, SafeHtml>()\n  indicatorStyle: Record<string, string> = { opacity: '0' }\n\n  get resolvedValue(): string {\n    if (this.value !== undefined) return this.value\n    if (this.modelValue !== '') return this.modelValue\n    return this.defaultValue\n  }\n\n  get hostClass(): string {\n    return cn(\n      'border-border bg-background/95 z-50 flex items-stretch justify-around border-t backdrop-blur-sm',\n      this.fixed ? 'fixed inset-x-0 bottom-0' : 'relative',\n      this.safeArea && 'pb-[env(safe-area-inset-bottom)]',\n      this.className,\n    )\n  }\n\n  isActive(item: BottomNavItem): boolean {\n    return this.resolvedValue === item.value\n  }\n\n  itemClass(item: BottomNavItem): string {\n    return 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      this.isActive(item) ? this.activeColor : 'text-muted-foreground hover:text-foreground',\n    )\n  }\n\n  labelClass(item: BottomNavItem): string {\n    return cn(\n      'max-w-full truncate px-1 transition-[opacity,font-weight] duration-200 motion-reduce:transition-none',\n      this.isActive(item) ? 'font-medium' : 'font-normal',\n    )\n  }\n\n  onSelect(item: BottomNavItem): void {\n    this.modelValue = item.value\n    this.value = item.value\n    this.valueChange.emit(item.value)\n    this.onValueChange.emit(item.value)\n    this.modelValueChange.emit(item.value)\n    this.select.emit(item)\n    queueMicrotask(() => this.updateIndicator())\n  }\n\n  isSvg(val: unknown): boolean {\n    return typeof val === 'string' && val.includes('<svg')\n  }\n\n  isTemplate(val: unknown): boolean {\n    return val instanceof TemplateRef\n  }\n\n  sanitize(svg: string): SafeHtml | string {\n    if (!this.sanitizer) return svg\n    let safe = this.sanitizedCache.get(svg)\n    if (!safe) {\n      safe = this.sanitizer.bypassSecurityTrustHtml(svg)\n      this.sanitizedCache.set(svg, safe)\n    }\n    return safe\n  }\n\n  ngAfterViewInit(): void {\n    this.updateIndicator()\n    if (this.el && typeof ResizeObserver !== 'undefined') {\n      this.ro = new ResizeObserver(() => this.updateIndicator())\n      const root = this.el.nativeElement\n      this.ro.observe(root)\n      root.querySelectorAll('[data-slot=\"bottom-navigation-item\"]').forEach((el) => this.ro?.observe(el))\n    }\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    // Track item identity so layout swaps remount without a false slide (React parity).\n    if (changes['items']) {\n      const key = this.items.map((i) => i.value).join('\\0')\n      if (key !== this.prevItemsKey) {\n        this.prevItemsKey = key\n        this.firstPosition = true\n      }\n    }\n    if (changes['showIndicator']) this.firstPosition = true\n    if (changes['value'] || changes['modelValue'] || changes['items'] || changes['showIndicator']) {\n      queueMicrotask(() => this.updateIndicator())\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.ro?.disconnect()\n    this.ro = null\n  }\n\n  private motionSafeTransition(): string {\n    if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n      return 'none'\n    }\n    return this.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  private updateIndicator(): void {\n    if (!this.showIndicator || !this.el) {\n      this.indicatorStyle = { opacity: '0' }\n      return\n    }\n    const root = this.el.nativeElement\n    const activeItem = root.querySelector<HTMLElement>('[data-slot=\"bottom-navigation-item\"][data-active]')\n    if (!activeItem) {\n      this.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    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 = this.motionSafeTransition()\n\n    this.indicatorStyle = {\n      width: `${pillW}px`,\n      height: `${pillH}px`,\n      transform: `translate3d(${left}px, ${top}px, 0)`,\n      opacity: '1',\n      transition,\n    }\n    this.firstPosition = false\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/bottom-navigation/bottom-navigation.component.ts"
    },
    {
      "path": "packages/registry-angular/components/bottom-navigation/index.ts",
      "content": "export { UiBottomNavigationComponent, type BottomNavItem } from './bottom-navigation.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/bottom-navigation/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Mobile bottom tab bar with icon + label items. Supports v-model for the active item, an active color, fixed positioning at the viewport bottom, badges on items, and a `to` prop for vue-router integration.",
  "categories": [
    "navigation"
  ]
}