{
  "name": "calendar",
  "type": "registry:component",
  "files": [
    {
      "name": "calendar.component.ts",
      "content": "import {\n  ChangeDetectionStrategy,\n  Component,\n  computed,\n  forwardRef,\n  input,\n  linkedSignal,\n  model,\n  viewChild,\n  ViewEncapsulation,\n} from '@angular/core';\nimport { outputFromObservable, outputToObservable } from '@angular/core/rxjs-interop';\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms';\n\nimport type { ClassValue } from 'clsx';\nimport { filter, map } from 'rxjs';\n\nimport { ZardCalendarGridComponent } from '@/shared/components/calendar/calendar-grid.component';\nimport { ZardCalendarNavigationComponent } from '@/shared/components/calendar/calendar-navigation.component';\nimport type { CalendarMode, CalendarValue } from '@/shared/components/calendar/calendar.types';\nimport {\n  generateCalendarDays,\n  getSelectedDatesArray,\n  isSameDay,\n  makeSafeDate,\n  normalizeCalendarValue,\n} from '@/shared/components/calendar/calendar.utils';\nimport { calendarVariants } from '@/shared/components/calendar/calendar.variants';\nimport { mergeClasses, noopFn } from '@/shared/utils/merge-classes';\n\n@Component({\n  selector: 'z-calendar, [z-calendar]',\n  imports: [ZardCalendarNavigationComponent, ZardCalendarGridComponent],\n  template: `\n    <div [class]=\"classes()\">\n      <z-calendar-navigation\n        [currentMonth]=\"currentMonthValue()\"\n        [currentYear]=\"currentYearValue()\"\n        [minDate]=\"minDate()\"\n        [maxDate]=\"maxDate()\"\n        [disabled]=\"disabled()\"\n        (monthChange)=\"onMonthChange($event)\"\n        (yearChange)=\"onYearChange($event)\"\n        (previousMonth)=\"previousMonth()\"\n        (nextMonth)=\"nextMonth()\"\n      />\n\n      <z-calendar-grid\n        [calendarDays]=\"calendarDays()\"\n        [disabled]=\"disabled()\"\n        (dateSelect)=\"onDateSelect($event)\"\n        (previousMonth)=\"onGridPreviousMonth($event)\"\n        (nextMonth)=\"onGridNextMonth($event)\"\n        (navigateYear)=\"onNavigateYear($event)\"\n      />\n    </div>\n  `,\n  providers: [\n    {\n      provide: NG_VALUE_ACCESSOR,\n      useExisting: forwardRef(() => ZardCalendarComponent),\n      multi: true,\n    },\n  ],\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n  host: {\n    '[attr.tabindex]': '0',\n  },\n  exportAs: 'zCalendar',\n})\nexport class ZardCalendarComponent implements ControlValueAccessor {\n  private readonly gridRef = viewChild.required(ZardCalendarGridComponent);\n\n  // Public method to reset navigation (useful for date-picker)\n  resetNavigation(): void {\n    const value = this.currentDate();\n    this.currentMonthValue.set(value.getMonth().toString());\n    this.currentYearValue.set(value.getFullYear().toString());\n    this.gridRef().setFocusedDayIndex(-1);\n  }\n\n  // Public inputs\n  readonly class = input<ClassValue>('');\n  readonly zMode = input<CalendarMode>('single');\n  readonly value = model<CalendarValue>(null);\n  readonly minDate = input<Date | null>(null);\n  readonly maxDate = input<Date | null>(null);\n  readonly disabled = model<boolean>(false);\n\n  // Public outputs\n  readonly dateChange = outputFromObservable(\n    outputToObservable(this.value).pipe(\n      map(v => normalizeCalendarValue(v)),\n      filter((v): v is NonNullable<CalendarValue> => v !== null),\n    ),\n  );\n\n  private onChange: (value: CalendarValue) => void = noopFn;\n  private onTouched: () => void = noopFn;\n\n  // Internal state\n  private readonly normalizedValue = computed(() => normalizeCalendarValue(this.value()));\n  private readonly currentDate = computed(() => {\n    const val = this.normalizedValue();\n    const mode = this.zMode();\n\n    if (!val) {\n      return new Date();\n    }\n\n    // For single mode, val is Date | null\n    if (mode === 'single') {\n      return val as Date;\n    }\n\n    // For multiple/range mode, val is Date[]\n    if (Array.isArray(val) && val.length > 0) {\n      return val[0];\n    }\n\n    return new Date();\n  });\n\n  protected readonly currentMonthValue = linkedSignal(() => this.currentDate().getMonth().toString());\n  protected readonly currentYearValue = linkedSignal(() => this.currentDate().getFullYear().toString());\n\n  protected readonly classes = computed(() => mergeClasses(calendarVariants(), this.class()));\n\n  protected readonly calendarDays = computed(() => {\n    const currentDate = this.currentDate();\n    const navigationDate = makeSafeDate(\n      Number.parseInt(this.currentYearValue()),\n      Number.parseInt(this.currentMonthValue()),\n      currentDate.getDate(),\n    );\n    const selectedDate = Number.isNaN(navigationDate.getTime()) ? currentDate : navigationDate;\n\n    return generateCalendarDays({\n      year: selectedDate.getFullYear(),\n      month: selectedDate.getMonth(),\n      mode: this.zMode(),\n      selectedDates: getSelectedDatesArray(this.normalizedValue(), this.zMode()),\n      minDate: this.minDate(),\n      maxDate: this.maxDate(),\n      disabled: this.disabled(),\n    });\n  });\n\n  protected onMonthChange(monthIndex: string | string[]): void {\n    if (Array.isArray(monthIndex)) {\n      console.warn('Calendar received array for month selection, expected single value. Ignoring:', monthIndex);\n      return;\n    }\n\n    if (!monthIndex?.trim()) {\n      console.warn('Invalid month index received:', monthIndex);\n      return;\n    }\n\n    const parsedMonth = Number.parseInt(monthIndex, 10);\n    if (Number.isNaN(parsedMonth) || parsedMonth < 0 || parsedMonth > 11) {\n      console.warn('Invalid month value:', monthIndex, 'parsed as:', parsedMonth);\n      return;\n    }\n\n    const currentDate = this.currentDate();\n    const selectedYear = Number.parseInt(this.currentYearValue());\n    const newDate = makeSafeDate(Number.isNaN(selectedYear) ? currentDate.getFullYear() : selectedYear, parsedMonth, 1);\n    this.currentMonthValue.set(newDate.getMonth().toString());\n    this.gridRef().setFocusedDayIndex(-1);\n  }\n\n  protected onYearChange(year: string | string[]): void {\n    if (Array.isArray(year)) {\n      console.warn('Calendar received array for year selection, expected single value. Ignoring:', year);\n      return;\n    }\n\n    if (!year?.trim()) {\n      console.warn('Invalid year received:', year);\n      return;\n    }\n\n    const parsedYear = Number.parseInt(year, 10);\n    if (Number.isNaN(parsedYear) || parsedYear < 1900 || parsedYear > 2100) {\n      console.warn('Invalid year value:', year, 'parsed as:', parsedYear);\n      return;\n    }\n\n    const currentDate = this.currentDate();\n    const selectedMonth = Number.parseInt(this.currentMonthValue());\n    const newDate = makeSafeDate(parsedYear, Number.isNaN(selectedMonth) ? currentDate.getMonth() : selectedMonth, 1);\n    this.currentYearValue.set(newDate.getFullYear().toString());\n    this.gridRef().setFocusedDayIndex(-1);\n  }\n\n  protected previousMonth(): void {\n    const month = Number.parseInt(this.currentMonthValue());\n    const year = Number.parseInt(this.currentYearValue());\n\n    const date = makeSafeDate(year, month - 1, 1);\n\n    this.currentMonthValue.set(date.getMonth().toString());\n    this.currentYearValue.set(date.getFullYear().toString());\n\n    this.gridRef().setFocusedDayIndex(-1);\n  }\n\n  protected nextMonth(): void {\n    const month = Number.parseInt(this.currentMonthValue());\n    const year = Number.parseInt(this.currentYearValue());\n\n    const date = makeSafeDate(year, month + 1, 1);\n\n    this.currentMonthValue.set(date.getMonth().toString());\n    this.currentYearValue.set(date.getFullYear().toString());\n\n    this.gridRef().setFocusedDayIndex(-1);\n  }\n\n  protected onNavigateYear(direction: number): void {\n    const current = this.currentDate();\n    const month = Number.parseInt(this.currentMonthValue());\n    const year = Number.parseInt(this.currentYearValue());\n    const baseYear = Number.isNaN(year) ? current.getFullYear() : year;\n    const baseMonth = Number.isNaN(month) ? current.getMonth() : month;\n    const newDate = makeSafeDate(baseYear + direction, baseMonth, 1);\n    this.currentYearValue.set(newDate.getFullYear().toString());\n    setTimeout(() => this.gridRef().resetFocus(), 0);\n  }\n\n  protected onGridPreviousMonth(event: { position: string; dayOfWeek: number }): void {\n    this.previousMonth();\n    setTimeout(() => this.resetFocusAfterNavigation(event.position, event.dayOfWeek), 0);\n  }\n\n  protected onGridNextMonth(event: { position: string; dayOfWeek: number }): void {\n    this.nextMonth();\n    setTimeout(() => this.resetFocusAfterNavigation(event.position, event.dayOfWeek), 0);\n  }\n\n  protected onDateSelect(event: { date: Date; index: number }): void {\n    this.selectDate(event.date);\n  }\n\n  private selectDate(date: Date): void {\n    if (this.disabled()) {\n      return;\n    }\n\n    const mode = this.zMode();\n    const currentValue = this.normalizedValue();\n\n    if (mode === 'single') {\n      this.value.set(date);\n    } else if (mode === 'multiple') {\n      const selectedDates = Array.isArray(currentValue) ? [...currentValue] : [];\n      const existingIndex = selectedDates.findIndex(d => isSameDay(d, date));\n\n      if (existingIndex >= 0) {\n        // Remove date if already selected\n        selectedDates.splice(existingIndex, 1);\n      } else {\n        // Add date\n        selectedDates.push(date);\n      }\n\n      this.value.set(selectedDates.length > 0 ? selectedDates : null);\n    } else if (mode === 'range') {\n      const selectedDates = Array.isArray(currentValue) ? [...currentValue] : [];\n\n      if (selectedDates.length === 0) {\n        // First date selected - set as range start\n        this.value.set([date]);\n      } else if (selectedDates.length === 1) {\n        // Second date selected - complete the range\n        const start = selectedDates[0];\n        if (date.getTime() < start.getTime()) {\n          // New date is before start, swap them\n          this.value.set([date, start]);\n        } else if (isSameDay(date, start)) {\n          // Same date clicked, reset\n          this.value.set(null);\n        } else {\n          // New date is after start\n          this.value.set([start, date]);\n        }\n      } else {\n        // Range already complete, start new range\n        this.value.set([date]);\n      }\n    }\n\n    this.onChange(this.normalizedValue());\n    this.onTouched();\n  }\n\n  private resetFocusAfterNavigation(position = 'default', dayOfWeek = -1): void {\n    const days = this.calendarDays();\n    let targetIndex = -1;\n\n    switch (position) {\n      case 'first':\n        // Focus first enabled day\n        targetIndex = days.findIndex(day => !day.isDisabled);\n        break;\n      case 'last':\n        // Focus last enabled day\n        for (let i = days.length - 1; i >= 0; i--) {\n          if (!days[i].isDisabled) {\n            targetIndex = i;\n            break;\n          }\n        }\n        break;\n      case 'firstWeek':\n        // Focus same day of week in first week\n        if (dayOfWeek >= 0 && dayOfWeek < 7) {\n          targetIndex = this.findEnabledInRange(dayOfWeek, 0, days);\n        }\n        break;\n      case 'lastWeek':\n        // Focus same day of week in last week\n        if (dayOfWeek >= 0) {\n          const lastWeekStart = Math.floor((days.length - 1) / 7) * 7;\n          const targetIdx = Math.min(lastWeekStart + dayOfWeek, days.length - 1);\n          targetIndex = this.findEnabledInRange(targetIdx, days.length - 1, days);\n        }\n        break;\n      default: {\n        // Default priority: selected > today > first enabled\n        const selectedIndex = days.findIndex(day => day.isSelected);\n        const todayIndex = days.findIndex(day => day.isToday && day.isCurrentMonth);\n        const firstEnabledIndex = days.findIndex(day => day.isCurrentMonth && !day.isDisabled);\n\n        targetIndex =\n          selectedIndex >= 0 ? selectedIndex : todayIndex >= 0 ? todayIndex : Math.max(firstEnabledIndex, 0);\n        break;\n      }\n    }\n\n    if (targetIndex >= 0) {\n      this.gridRef().setFocusedDayIndex(targetIndex);\n    }\n  }\n\n  private findEnabledInRange(start: number, fallback: number, days: { isDisabled: boolean }[]): number {\n    const clampedStart = Math.max(0, Math.min(start, days.length - 1));\n    const clampedFallback = Math.max(0, Math.min(fallback, days.length - 1));\n\n    // Search forward from start\n    for (let i = clampedStart; i < days.length; i++) {\n      if (!days[i].isDisabled) {\n        return i;\n      }\n    }\n    // Search backward from start\n    for (let i = clampedStart - 1; i >= 0; i--) {\n      if (!days[i].isDisabled) {\n        return i;\n      }\n    }\n\n    return clampedFallback;\n  }\n\n  writeValue(value: CalendarValue): void {\n    this.value.set(value);\n  }\n\n  registerOnChange(fn: (value: CalendarValue) => void): void {\n    this.onChange = fn;\n  }\n\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn;\n  }\n\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled.set(isDisabled);\n  }\n}\n"
    },
    {
      "name": "calendar-grid.component.ts",
      "content": "import {\n  ChangeDetectionStrategy,\n  Component,\n  computed,\n  type ElementRef,\n  input,\n  output,\n  signal,\n  viewChild,\n  ViewEncapsulation,\n} from '@angular/core';\n\nimport { mergeClasses } from '@/shared/utils/merge-classes';\n\nimport type { CalendarDay } from './calendar.types';\nimport { calendarWeekdays, getDayAriaLabel, getDayId } from './calendar.utils';\nimport { calendarDayButtonVariants, calendarDayVariants, calendarWeekdayVariants } from './calendar.variants';\n\n@Component({\n  selector: 'z-calendar-grid',\n  template: `\n    <div #gridContainer>\n      <!-- Weekdays Header -->\n      <div class=\"grid w-fit grid-cols-7 text-center\" role=\"row\">\n        @for (weekday of weekdays; track weekday) {\n          <div [class]=\"weekdayClasses()\" role=\"columnheader\">\n            {{ weekday }}\n          </div>\n        }\n      </div>\n\n      <!-- Calendar Days Grid -->\n      <div class=\"mt-2 grid w-fit auto-rows-min grid-cols-7 gap-0\" role=\"rowgroup\">\n        @for (day of calendarDays(); track day.date.getTime(); let i = $index) {\n          <div [class]=\"dayContainerClasses()\" role=\"gridcell\">\n            <button\n              type=\"button\"\n              [id]=\"getDayId(i)\"\n              [class]=\"dayButtonClasses(day)\"\n              (click)=\"onDayClick(day.date, i)\"\n              [disabled]=\"day.isDisabled\"\n              [attr.aria-selected]=\"day.isSelected\"\n              [attr.aria-label]=\"getDayAriaLabel(day)\"\n              [attr.tabindex]=\"getFocusedDayIndex() === i ? 0 : -1\"\n              role=\"button\"\n            >\n              {{ day.date.getDate() }}\n            </button>\n          </div>\n        }\n      </div>\n    </div>\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n  host: {\n    class: 'flex justify-center',\n    '[attr.role]': '\"grid\"',\n    '(keydown.{arrowleft,arrowright,arrowup,arrowdown,home,end,pageup,pagedown,enter,space}.prevent)':\n      'onKeyDown($event)',\n  },\n  exportAs: 'zCalendarGrid',\n})\nexport class ZardCalendarGridComponent {\n  private readonly gridContainer = viewChild.required<ElementRef<HTMLElement>>('gridContainer');\n\n  // Inputs\n  readonly calendarDays = input.required<CalendarDay[]>();\n  readonly disabled = input<boolean>(false);\n\n  // Outputs\n  readonly dateSelect = output<{ date: Date; index: number }>();\n  readonly previousMonth = output<{ position: string; dayOfWeek: number }>();\n  readonly nextMonth = output<{ position: string; dayOfWeek: number }>();\n  readonly navigateYear = output<number>();\n\n  readonly weekdays = calendarWeekdays;\n\n  private readonly focusedDayIndex = signal<number>(-1);\n\n  protected readonly weekdayClasses = computed(() => mergeClasses(calendarWeekdayVariants()));\n\n  protected readonly dayContainerClasses = computed(() => mergeClasses(calendarDayVariants()));\n\n  protected dayButtonClasses(day: CalendarDay): string {\n    return mergeClasses(\n      calendarDayButtonVariants({\n        selected: day.isSelected,\n        today: day.isToday,\n        outside: !day.isCurrentMonth,\n        disabled: day.isDisabled,\n        rangeStart: day.isRangeStart ?? false,\n        rangeEnd: day.isRangeEnd ?? false,\n        inRange: day.isInRange ?? false,\n      }),\n    );\n  }\n\n  protected onDayClick(date: Date, index: number): void {\n    if (this.disabled()) {\n      return;\n    }\n    this.focusedDayIndex.set(index);\n    this.dateSelect.emit({ date, index });\n  }\n\n  protected getDayId(index: number): string {\n    return getDayId(index);\n  }\n\n  protected getDayAriaLabel(day: CalendarDay): string {\n    return getDayAriaLabel(day);\n  }\n\n  protected getFocusedDayIndex(): number {\n    const focused = this.focusedDayIndex();\n    if (focused >= 0) {\n      return focused;\n    }\n\n    // Default focus to selected date or today\n    const days = this.calendarDays();\n    const selectedIndex = days.findIndex(day => day.isSelected);\n    if (selectedIndex >= 0) {\n      return selectedIndex;\n    }\n\n    const todayIndex = days.findIndex(day => day.isToday && day.isCurrentMonth);\n    if (todayIndex >= 0) {\n      return todayIndex;\n    }\n\n    // Fall back to first enabled day of current month\n    const firstCurrentMonthIndex = days.findIndex(day => day.isCurrentMonth && !day.isDisabled);\n    return firstCurrentMonthIndex >= 0 ? firstCurrentMonthIndex : 0;\n  }\n\n  /**\n   * Public method to set focus on a specific day index\n   */\n  setFocusedDayIndex(index: number): void {\n    this.focusedDayIndex.set(index);\n    this.setFocus(index);\n  }\n\n  /**\n   * Public method to reset focus based on priority\n   */\n  resetFocus(): void {\n    const targetIndex = this.getFocusedDayIndex();\n    this.setFocus(targetIndex);\n  }\n\n  onKeyDown(e: Event): void {\n    if (this.disabled()) {\n      return;\n    }\n\n    const event = e as KeyboardEvent;\n    const days = this.calendarDays();\n    if (days.length === 0) {\n      return;\n    }\n\n    const currentIndex = this.getFocusedDayIndex();\n    let newIndex: number | null = null;\n\n    switch (event.key) {\n      case 'ArrowLeft':\n        newIndex = this.navigate(currentIndex, -1, days);\n        break;\n      case 'ArrowRight':\n        newIndex = this.navigate(currentIndex, 1, days);\n        break;\n      case 'ArrowUp':\n        newIndex = this.navigate(currentIndex, -7, days);\n        break;\n      case 'ArrowDown':\n        newIndex = this.navigate(currentIndex, 7, days);\n        break;\n      case 'Home':\n        newIndex = this.findEnabledInRange(\n          Math.floor(currentIndex / 7) * 7,\n          Math.floor(currentIndex / 7) * 7 + 6,\n          days,\n        );\n        break;\n      case 'End':\n        newIndex = this.findEnabledInRange(\n          Math.floor(currentIndex / 7) * 7 + 6,\n          Math.floor(currentIndex / 7) * 7,\n          days,\n          true,\n        );\n        break;\n      case 'PageUp':\n        if (event.ctrlKey) {\n          this.navigateYear.emit(-1);\n        } else {\n          this.previousMonth.emit({ position: 'default', dayOfWeek: -1 });\n        }\n        break;\n      case 'PageDown':\n        if (event.ctrlKey) {\n          this.navigateYear.emit(1);\n        } else {\n          this.nextMonth.emit({ position: 'default', dayOfWeek: -1 });\n        }\n        break;\n      case 'Enter':\n      case ' ': {\n        const focusedDay = days[currentIndex];\n        if (focusedDay && !focusedDay.isDisabled) {\n          this.dateSelect.emit({ date: focusedDay.date, index: currentIndex });\n        }\n        break;\n      }\n      default:\n        break;\n    }\n\n    if (newIndex !== null && newIndex !== currentIndex) {\n      this.setFocus(newIndex);\n    }\n  }\n\n  private navigate(currentIndex: number, step: number, days: CalendarDay[]): number | null {\n    const targetIndex = currentIndex + step;\n\n    // If within bounds, find enabled day\n    if (targetIndex >= 0 && targetIndex < days.length) {\n      return this.findEnabledInRange(targetIndex, currentIndex, days);\n    }\n\n    // Handle month boundaries\n    const dayOfWeek = currentIndex % 7;\n\n    if (step === -1) {\n      // Going left - navigate to previous month, focus last day\n      this.previousMonth.emit({ position: 'last', dayOfWeek: -1 });\n    } else if (step === 1) {\n      // Going right - navigate to next month, focus first day\n      this.nextMonth.emit({ position: 'first', dayOfWeek: -1 });\n    } else if (step === -7) {\n      // Going up - navigate to previous month, preserve column\n      this.previousMonth.emit({ position: 'lastWeek', dayOfWeek });\n    } else if (step === 7) {\n      // Going down - navigate to next month, preserve column\n      this.nextMonth.emit({ position: 'firstWeek', dayOfWeek });\n    }\n\n    return null;\n  }\n\n  private findEnabledInRange(start: number, fallback: number, days: CalendarDay[], reverse = false): number {\n    const clampedStart = Math.max(0, Math.min(start, days.length - 1));\n    const clampedFallback = Math.max(0, Math.min(fallback, days.length - 1));\n\n    if (!reverse) {\n      // Search forward from start\n      for (let i = clampedStart; i < days.length; i++) {\n        if (!days[i].isDisabled) {\n          return i;\n        }\n      }\n      // Search backward from start\n      for (let i = clampedStart - 1; i >= 0; i--) {\n        if (!days[i].isDisabled) {\n          return i;\n        }\n      }\n    } else {\n      // Search backward from start\n      for (let i = clampedStart; i >= 0; i--) {\n        if (!days[i].isDisabled) {\n          return i;\n        }\n      }\n      // Search forward from start\n      for (let i = clampedStart + 1; i < days.length; i++) {\n        if (!days[i].isDisabled) {\n          return i;\n        }\n      }\n    }\n\n    return clampedFallback;\n  }\n\n  private setFocus(index: number): void {\n    this.focusedDayIndex.set(index);\n    setTimeout(() => {\n      const dayElement = this.gridContainer()?.nativeElement.querySelector(`#${getDayId(index)}`) as HTMLElement;\n      dayElement?.focus();\n    }, 0);\n  }\n}\n"
    },
    {
      "name": "calendar-navigation.component.ts",
      "content": "import { ChangeDetectionStrategy, Component, computed, input, output, ViewEncapsulation } from '@angular/core';\n\nimport { NgIcon, provideIcons } from '@ng-icons/core';\nimport { lucideChevronLeft, lucideChevronRight } from '@ng-icons/lucide';\n\nimport { calendarMonths } from '@/shared/components/calendar/calendar.utils';\nimport { mergeClasses } from '@/shared/utils/merge-classes';\n\nimport { calendarNavVariants } from './calendar.variants';\nimport { ZardButtonComponent } from '../button/button.component';\nimport { ZardSelectItemComponent } from '../select/select-item.component';\nimport { ZardSelectComponent } from '../select/select.component';\n\n@Component({\n  selector: 'z-calendar-navigation',\n  imports: [ZardButtonComponent, NgIcon, ZardSelectComponent, ZardSelectItemComponent],\n  template: `\n    <div [class]=\"navClasses()\">\n      <button\n        type=\"button\"\n        z-button\n        zType=\"ghost\"\n        zSize=\"sm\"\n        (click)=\"onPreviousClick()\"\n        [disabled]=\"isPreviousDisabled()\"\n        aria-label=\"Previous month\"\n        class=\"size-7 p-0\"\n      >\n        <ng-icon name=\"lucideChevronLeft\" class=\"size-3.5!\" />\n      </button>\n\n      <!-- Month and Year Selectors -->\n      <div class=\"flex items-center space-x-2\">\n        <!-- Month Select -->\n        <z-select [zValue]=\"currentMonth()\" [zLabel]=\"currentMonthName()\" (zSelectionChange)=\"onMonthChange($event)\">\n          @for (month of months; track month) {\n            <z-select-item [zValue]=\"$index.toString()\">{{ month }}</z-select-item>\n          }\n        </z-select>\n\n        <!-- Year Select -->\n        <z-select [zValue]=\"currentYear()\" [zLabel]=\"currentYear()\" (zSelectionChange)=\"onYearChange($event)\">\n          @for (year of availableYears(); track year) {\n            <z-select-item [zValue]=\"year.toString()\">{{ year }}</z-select-item>\n          }\n        </z-select>\n      </div>\n\n      <button\n        type=\"button\"\n        z-button\n        zType=\"ghost\"\n        zSize=\"sm\"\n        (click)=\"onNextClick()\"\n        [disabled]=\"isNextDisabled()\"\n        aria-label=\"Next month\"\n        class=\"size-7 p-0\"\n      >\n        <ng-icon name=\"lucideChevronRight\" class=\"size-3.5!\" />\n      </button>\n    </div>\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n  encapsulation: ViewEncapsulation.None,\n  viewProviders: [provideIcons({ lucideChevronLeft, lucideChevronRight })],\n  exportAs: 'zCalendarNavigation',\n})\nexport class ZardCalendarNavigationComponent {\n  // Inputs\n  readonly currentMonth = input.required<string>();\n  readonly currentYear = input.required<string>();\n  readonly minDate = input<Date | null>(null);\n  readonly maxDate = input<Date | null>(null);\n  readonly disabled = input<boolean>(false);\n\n  // Outputs\n  readonly monthChange = output<string>();\n  readonly yearChange = output<string>();\n  readonly previousMonth = output<void>();\n  readonly nextMonth = output<void>();\n  readonly months = calendarMonths;\n\n  protected readonly navClasses = computed(() => mergeClasses(calendarNavVariants()));\n\n  protected readonly availableYears = computed(() => {\n    const minYear = this.minDate()?.getFullYear() ?? new Date().getFullYear() - 10;\n    const maxYear = this.maxDate()?.getFullYear() ?? new Date().getFullYear() + 10;\n    const years = [];\n    for (let i = minYear; i <= maxYear; i++) {\n      years.push(i);\n    }\n    return years;\n  });\n\n  protected readonly currentMonthName = computed(() => {\n    const selectedMonth = Number.parseInt(this.currentMonth());\n    if (!Number.isNaN(selectedMonth) && this.months[selectedMonth]) {\n      return this.months[selectedMonth];\n    }\n    return this.months[new Date().getMonth()];\n  });\n\n  protected readonly isPreviousDisabled = computed(() => {\n    if (this.disabled()) {\n      return true;\n    }\n\n    const minDate = this.minDate();\n    if (!minDate) {\n      return false;\n    }\n\n    const currentMonth = Number.parseInt(this.currentMonth());\n    const currentYear = Number.parseInt(this.currentYear());\n    const lastDayOfPreviousMonth = new Date(currentYear, currentMonth, 0);\n\n    return lastDayOfPreviousMonth.getTime() < minDate.getTime();\n  });\n\n  protected readonly isNextDisabled = computed(() => {\n    if (this.disabled()) {\n      return true;\n    }\n\n    const maxDate = this.maxDate();\n    if (!maxDate) {\n      return false;\n    }\n\n    const currentMonth = Number.parseInt(this.currentMonth());\n    const currentYear = Number.parseInt(this.currentYear());\n    const nextMonth = new Date(currentYear, currentMonth + 1, 1);\n\n    return nextMonth.getTime() > maxDate.getTime();\n  });\n\n  protected onPreviousClick(): void {\n    this.previousMonth.emit();\n  }\n\n  protected onNextClick(): void {\n    this.nextMonth.emit();\n  }\n\n  protected onMonthChange(month: string | string[]): void {\n    if (Array.isArray(month)) {\n      console.warn('Calendar navigation received array for month selection, expected single value. Ignoring:', month);\n      return;\n    }\n    this.monthChange.emit(month);\n  }\n\n  protected onYearChange(year: string | string[]): void {\n    if (Array.isArray(year)) {\n      console.warn('Calendar navigation received array for year selection, expected single value. Ignoring:', year);\n      return;\n    }\n    this.yearChange.emit(year);\n  }\n}\n"
    },
    {
      "name": "calendar.types.ts",
      "content": "export type CalendarMode = 'single' | 'multiple' | 'range';\nexport type CalendarValue = Date | Date[] | null;\n\nexport interface CalendarDay {\n  date: Date;\n  isCurrentMonth: boolean;\n  isToday: boolean;\n  isSelected: boolean;\n  isDisabled: boolean;\n  isRangeStart?: boolean;\n  isRangeEnd?: boolean;\n  isInRange?: boolean;\n  id?: string;\n}\n\nexport interface CalendarDayConfig {\n  year: number;\n  month: number;\n  mode: CalendarMode;\n  selectedDates: Date[];\n  minDate: Date | null;\n  maxDate: Date | null;\n  disabled: boolean;\n}\n"
    },
    {
      "name": "calendar.utils.ts",
      "content": "import type { CalendarDay, CalendarDayConfig, CalendarMode, CalendarValue } from './calendar.types';\n\nexport const calendarMonths = [\n  'Jan',\n  'Feb',\n  'Mar',\n  'Apr',\n  'May',\n  'Jun',\n  'Jul',\n  'Aug',\n  'Sep',\n  'Oct',\n  'Nov',\n  'Dec',\n] as const;\n\nexport const calendarWeekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const;\n\n/**\n * Checks if two dates represent the same day (ignoring time)\n */\nexport function isSameDay(date1: Date, date2: Date): boolean {\n  return (\n    date1.getFullYear() === date2.getFullYear() &&\n    date1.getMonth() === date2.getMonth() &&\n    date1.getDate() === date2.getDate()\n  );\n}\n\n/**\n * Checks if a date is disabled based on min/max constraints\n */\nexport function isDateDisabled(date: Date, minDate: Date | null, maxDate: Date | null): boolean {\n  if ((minDate && date < minDate) || (maxDate && date > maxDate)) {\n    return true;\n  }\n  return false;\n}\n\n/**\n * Generates calendar days for a given month with all selection states\n */\nexport function generateCalendarDays(config: CalendarDayConfig): CalendarDay[] {\n  const { year, month, mode, selectedDates, minDate, maxDate, disabled } = config;\n\n  const today = new Date();\n\n  // Get first day of the month\n  const firstDay = new Date(year, month, 1);\n  // Get last day of the month\n  const lastDay = new Date(year, month + 1, 0);\n\n  // Get the first day of the week for the first day of the month\n  const startDate = new Date(firstDay);\n  startDate.setDate(startDate.getDate() - startDate.getDay());\n\n  // Get the last day of the week for the last day of the month\n  const endDate = new Date(lastDay);\n  endDate.setDate(endDate.getDate() + (6 - endDate.getDay()));\n\n  const days: CalendarDay[] = [];\n  const currentWeekDate = new Date(startDate);\n\n  // For range mode, determine range start and end\n  let rangeStart: Date | null = null;\n  let rangeEnd: Date | null = null;\n  if (mode === 'range' && selectedDates.length > 0) {\n    rangeStart = selectedDates[0];\n    rangeEnd = selectedDates.length > 1 ? selectedDates[1] : null;\n  }\n\n  while (currentWeekDate <= endDate) {\n    const date = new Date(currentWeekDate);\n    const isCurrentMonth = date.getMonth() === month;\n    const isToday = isSameDay(date, today);\n    const isDisabledDate = disabled || isDateDisabled(date, minDate, maxDate);\n\n    // Determine if date is selected\n    let isSelected = false;\n    let isRangeStart = false;\n    let isRangeEnd = false;\n    let isInRange = false;\n\n    if (mode === 'single') {\n      isSelected = selectedDates.length > 0 && isSameDay(date, selectedDates[0]);\n    } else if (mode === 'multiple') {\n      isSelected = selectedDates.some(d => isSameDay(date, d));\n    } else if (mode === 'range') {\n      if (rangeStart && isSameDay(date, rangeStart)) {\n        isRangeStart = true;\n        isSelected = true;\n      }\n      if (rangeEnd && isSameDay(date, rangeEnd)) {\n        isRangeEnd = true;\n        isSelected = true;\n      }\n      if (rangeStart && rangeEnd && !isRangeStart && !isRangeEnd) {\n        // Check if date is between start and end\n        const dateTime = date.getTime();\n        const startTime = rangeStart.getTime();\n        const endTime = rangeEnd.getTime();\n        isInRange = dateTime > startTime && dateTime < endTime;\n      }\n    }\n\n    days.push({\n      date,\n      isCurrentMonth,\n      isToday,\n      isSelected,\n      isDisabled: isDisabledDate,\n      isRangeStart,\n      isRangeEnd,\n      isInRange,\n    });\n\n    currentWeekDate.setDate(currentWeekDate.getDate() + 1);\n  }\n\n  return days;\n}\n\n/**\n * Converts CalendarValue to array of Dates for easier processing\n */\nexport function getSelectedDatesArray(value: CalendarValue, mode: CalendarMode): Date[] {\n  if (!value) {\n    return [];\n  }\n\n  if (mode === 'single') {\n    return [value as Date];\n  }\n\n  if ((mode === 'multiple' || mode === 'range') && Array.isArray(value)) {\n    return value;\n  }\n\n  return [];\n}\n\n/**\n * Generates a unique ID for a calendar day\n */\nexport function getDayId(index: number): string {\n  return `calendar-day-${index}`;\n}\n\n/**\n * Generates an accessible ARIA label for a calendar day\n */\nexport function getDayAriaLabel(day: CalendarDay): string {\n  const dateStr = day.date.toLocaleDateString('en-US', {\n    weekday: 'long',\n    year: 'numeric',\n    month: 'long',\n    day: 'numeric',\n  });\n\n  const labels = [\n    dateStr,\n    day.isToday && 'Today',\n    day.isSelected && 'Selected',\n    day.isRangeStart && 'Range start',\n    day.isRangeEnd && 'Range end',\n    day.isInRange && 'In range',\n    !day.isCurrentMonth && 'Outside month',\n    day.isDisabled && 'Disabled',\n  ].filter(Boolean);\n\n  return labels.join(', ');\n}\n\n/**\n * Creates a date positioned safely at midday to avoid timezone-based\n * month/day shifts triggered by local DST or UTC conversions.\n *\n * Useful when constructing calendar/navigation dates where 00:00\n * may incorrectly roll the date backward or forward.\n */\nexport function makeSafeDate(year: number, month: number, day = 1): Date {\n  const date = new Date(year, month, day);\n  date.setHours(12, 0, 0, 0);\n  return date;\n}\n\n/**\n * Normalizes any calendar value into a valid Date or array of Dates.\n * Returns null for empty values, validates single Dates, converts arrays,\n * and attempts to parse any other type into a Date.\n */\nexport function normalizeCalendarValue(v: CalendarValue): CalendarValue {\n  if (!v) {\n    return v;\n  }\n\n  if (v instanceof Date) {\n    return toValidDate(v);\n  }\n\n  if (Array.isArray(v)) {\n    return v.reduce<Date[]>((acc, d) => {\n      const date = toValidDate(d);\n      if (date) {\n        acc.push(date);\n      }\n      return acc;\n    }, []);\n  }\n\n  return toValidDate(v);\n}\n\n/**\n * Converts any value into a valid Date.\n * If it is already a Date, it is returned as is.\n * If the conversion fails, null should be returned.\n */\nexport function toValidDate(value: unknown): Date | null {\n  if (value instanceof Date) {\n    return isNaN(value.getTime()) ? null : value;\n  }\n\n  if (typeof value === 'number' && value.toString().length === 8) {\n    const s = value.toString();\n    const y = +s.slice(0, 4);\n    const m = +s.slice(4, 6) - 1;\n    const d = +s.slice(6, 8);\n\n    return makeSafeDate(y, m, d);\n  }\n\n  if (typeof value === 'string' && /^\\d{8}$/.test(value)) {\n    const y = +value.slice(0, 4);\n    const m = +value.slice(4, 6) - 1;\n    const d = +value.slice(6, 8);\n\n    return makeSafeDate(y, m, d);\n  }\n\n  const date = new Date(value as string | number | Date);\n\n  if (isNaN(date.getTime())) {\n    return null;\n  }\n\n  return makeSafeDate(date.getFullYear(), date.getMonth(), date.getDate());\n}\n"
    },
    {
      "name": "calendar.variants.ts",
      "content": "import { cva } from 'class-variance-authority';\n\nexport const calendarVariants = cva('bg-background p-3 w-fit rounded-lg border');\n\nexport const calendarMonthVariants = cva('flex flex-col w-fit gap-4');\n\nexport const calendarNavVariants = cva('flex items-center justify-between gap-2 w-fit mb-4');\n\nexport const calendarNavButtonVariants = cva(\n  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground size-7 bg-transparent p-0 opacity-50 hover:opacity-100',\n);\n\nexport const calendarWeekdaysVariants = cva('flex');\n\nexport const calendarWeekdayVariants = cva('text-muted-foreground font-normal text-center text-[0.8rem] w-8');\n\nexport const calendarWeekVariants = cva('flex w-full mt-2');\n\nexport const calendarDayVariants = cva('p-0 relative focus-within:relative focus-within:z-20 flex mt-1 size-8 text-sm');\n\nexport const calendarDayButtonVariants = cva(\n  'p-0 font-normal flex items-center justify-center whitespace-nowrap rounded-md ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground size-full text-sm',\n  {\n    variants: {\n      selected: {\n        true: 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',\n        false: '',\n      },\n      today: {\n        true: 'bg-accent text-accent-foreground',\n        false: '',\n      },\n      outside: {\n        true: 'text-muted-foreground opacity-50',\n        false: '',\n      },\n      disabled: {\n        true: 'text-muted-foreground opacity-50 cursor-not-allowed',\n        false: '',\n      },\n      rangeStart: {\n        true: 'rounded-r-none bg-primary text-primary-foreground',\n        false: '',\n      },\n      rangeEnd: {\n        true: 'rounded-l-none bg-primary text-primary-foreground',\n        false: '',\n      },\n      inRange: {\n        true: 'rounded-none bg-accent hover:bg-accent',\n        false: '',\n      },\n    },\n    compoundVariants: [\n      {\n        today: true,\n        selected: false,\n        rangeStart: false,\n        rangeEnd: false,\n        inRange: false,\n        className: 'bg-accent text-accent-foreground',\n      },\n      {\n        today: true,\n        selected: true,\n        className: 'bg-primary text-primary-foreground',\n      },\n      {\n        rangeStart: true,\n        rangeEnd: true,\n        className: 'rounded-md bg-primary text-primary-foreground',\n      },\n    ],\n    defaultVariants: {\n      selected: false,\n      today: false,\n      outside: false,\n      disabled: false,\n      rangeStart: false,\n      rangeEnd: false,\n      inRange: false,\n    },\n  },\n);\n"
    },
    {
      "name": "index.ts",
      "content": "export * from './calendar.component';\nexport * from './calendar.variants';\nexport * from './calendar.types';\nexport * from './calendar.utils';\nexport * from './calendar-grid.component';\nexport * from './calendar-navigation.component';\n"
    }
  ],
  "registryDependencies": [
    "select"
  ],
  "demos": [
    {
      "name": "default.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardCalendarComponent } from '../calendar.component';\n\n@Component({\n  selector: 'z-demo-calendar-default',\n  imports: [ZardCalendarComponent],\n  standalone: true,\n  template: `\n    <z-calendar (dateChange)=\"onDateChange($event)\" />\n  `,\n})\nexport class ZardDemoCalendarDefaultComponent {\n  onDateChange(date: Date | Date[]) {\n    console.log('Selected date:', date);\n  }\n}\n"
    },
    {
      "name": "expand-year-selection-range.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardCalendarComponent } from '../calendar.component';\n\n@Component({\n  selector: 'z-demo-calendar-expand-year-selection-range',\n  imports: [ZardCalendarComponent],\n  standalone: true,\n  template: `\n    <div class=\"space-y-8\">\n      <div>\n        <h3 class=\"mb-3 text-sm font-medium\">Date of Birth</h3>\n        <z-calendar [minDate]=\"minDate\" [maxDate]=\"maxDate\" (dateChange)=\"onDateChange($event)\" />\n      </div>\n    </div>\n  `,\n})\nexport class ZardDemoCalendarExpandYearSelectionRangeComponent {\n  minDate = new Date(1950, 1, 1);\n  maxDate = new Date();\n\n  onDateChange(date: Date | Date[]) {\n    console.log('Selected date:', date);\n  }\n}\n"
    },
    {
      "name": "multiple.ts",
      "content": "import { Component, signal } from '@angular/core';\n\nimport { ZardCalendarComponent } from '../calendar.component';\n\n@Component({\n  selector: 'z-demo-calendar-multiple',\n  imports: [ZardCalendarComponent],\n  standalone: true,\n  template: `\n    <div class=\"space-y-4\">\n      <z-calendar zMode=\"multiple\" [(value)]=\"selectedDates\" (dateChange)=\"onDateChange($event)\" />\n\n      <div class=\"text-muted-foreground mt-2 text-sm\">\n        <p class=\"font-medium\">Selected ({{ selectedDates()?.length ?? 0 }}) date(s).</p>\n      </div>\n    </div>\n  `,\n})\nexport class ZardDemoCalendarMultipleComponent {\n  readonly selectedDates = signal<Date[] | null>(null);\n\n  onDateChange(dates: Date | Date[]) {\n    console.log('Selected dates:', dates);\n  }\n\n  formatDate(date: Date): string {\n    return date.toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });\n  }\n}\n"
    },
    {
      "name": "range.ts",
      "content": "import { Component, signal } from '@angular/core';\n\nimport { ZardCalendarComponent } from '../calendar.component';\n\n@Component({\n  selector: 'z-demo-calendar-range',\n  imports: [ZardCalendarComponent],\n  standalone: true,\n  template: `\n    <div class=\"space-y-4\">\n      <z-calendar zMode=\"range\" [(value)]=\"dateRange\" (dateChange)=\"onDateChange($event)\" />\n\n      <div class=\"bg-muted/50 mt-4 rounded-lg border p-4\">\n        <div class=\"space-y-1 text-sm\">\n          <div class=\"flex items-center gap-2\">\n            <span class=\"text-muted-foreground min-w-12\">From:</span>\n            <span class=\"font-medium\">{{ formatDate(dateRange(), 'start') }}</span>\n          </div>\n\n          <div class=\"flex items-center gap-2\">\n            <span class=\"text-muted-foreground min-w-12\">To:</span>\n            <span class=\"font-medium\">{{ formatDate(dateRange(), 'end') }}</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class ZardDemoCalendarRangeComponent {\n  readonly dateRange = signal<Date[] | null>(null);\n\n  onDateChange(dates: Date | Date[]) {\n    console.log('Selected range:', dates);\n  }\n\n  formatDate(date?: Date[] | null, label: 'start' | 'end' = 'start'): string {\n    if (!date || date?.length === 0) {\n      return 'N/A';\n    }\n\n    const targetDate = label === 'start' ? date[0] : date[1];\n    if (!targetDate) {\n      return 'N/A';\n    }\n\n    return targetDate.toLocaleDateString('en-US', {\n      weekday: 'short',\n      year: 'numeric',\n      month: 'short',\n      day: 'numeric',\n    });\n  }\n}\n"
    },
    {
      "name": "with-constraints.ts",
      "content": "import { Component } from '@angular/core';\n\nimport { ZardCalendarComponent } from '../calendar.component';\n\nconst DAYS_IN_FUTURE = 30;\nconst MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;\n\n@Component({\n  selector: 'z-demo-calendar-with-constraints',\n  imports: [ZardCalendarComponent],\n  standalone: true,\n  template: `\n    <div class=\"space-y-8\">\n      <div>\n        <h3 class=\"mb-3 text-sm font-medium\">With Min/Max Date</h3>\n        <z-calendar [minDate]=\"minDate\" [maxDate]=\"maxDate\" (dateChange)=\"onDateChange($event)\" />\n        <p class=\"text-muted-foreground mt-2 text-sm\">\n          Available dates: {{ minDate.toLocaleDateString() }} - {{ maxDate.toLocaleDateString() }}\n        </p>\n      </div>\n\n      <div>\n        <h3 class=\"mb-3 text-sm font-medium\">Disabled</h3>\n        <z-calendar [disabled]=\"true\" />\n      </div>\n    </div>\n  `,\n})\nexport class ZardDemoCalendarWithConstraintsComponent {\n  minDate = new Date();\n  maxDate = new Date(Date.now() + DAYS_IN_FUTURE * MILLISECONDS_PER_DAY);\n\n  constructor() {\n    // Set min date to today\n    this.minDate.setHours(0, 0, 0, 0);\n\n    // Set max date to 30 days from now\n    this.maxDate.setHours(23, 59, 59, 999);\n  }\n\n  onDateChange(date: Date | Date[]) {\n    console.log('Selected date:', date);\n  }\n}\n"
    }
  ]
}
