Calendar

PreviousNext

A date field component that enables users to choose and edit a date.

Docs
scrollxuicomponent

Preview

Loading preview…
components/ui/calendar.tsx
"use client"

import * as React from "react"
import type { Dispatch, SetStateAction } from "react"
import {
  ChevronDownIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

interface DateRange {
  from: Date | undefined
  to?: Date | undefined
}

interface Modifiers {
  [key: string]: Date[] | ((date: Date) => boolean) | { dayOfWeek?: number[], before?: Date, after?: Date }
}

interface FormatOptions {
  locale?: string
  [key: string]: unknown
}

interface CalendarBaseProps {
  className?: string
  classNames?: {
    [key: string]: string
  }

  showOutsideDays?: boolean
  showWeekNumber?: boolean
  numberOfMonths?: number
  captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years"
  fromYear?: number
  toYear?: number
  fromMonth?: Date
  toMonth?: Date
  yearOrder?: "asc" | "desc"

  disabled?: Date[] | ((date: Date) => boolean) | { before?: Date, after?: Date, dayOfWeek?: number[] }

  modifiers?: Modifiers
  modifiersClassNames?: { [key: string]: string }

  locale?: string
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
  firstWeekContainsDate?: 1 | 2 | 3 | 4 | 5 | 6 | 7

  labels?: {
    labelMonthDropdown?: () => string
    labelYearDropdown?: () => string
    labelNext?: () => string
    labelPrevious?: () => string
  }

  formatters?: {
    formatCaption?: (date: Date, options?: FormatOptions) => string
    formatDay?: (date: Date) => string
    formatMonthDropdown?: (date: Date) => string
    formatYearDropdown?: (date: Date) => string
    formatWeekNumber?: (weekNumber: number) => string
    formatWeekdayName?: (date: Date) => string
  }

  onMonthChange?: (date: Date) => void
  onDayClick?: (date: Date, modifiers: string[]) => void
  onDayMouseEnter?: (date: Date, modifiers: string[]) => void
  onDayMouseLeave?: (date: Date, modifiers: string[]) => void

  footer?: React.ReactNode
  components?: {
    DayButton?: React.ComponentType<CalendarDayButtonProps>
    [key: string]: React.ComponentType<CalendarDayButtonProps> | undefined
  }

  animate?: boolean
  dir?: "ltr" | "rtl"
  autoFocus?: boolean
  defaultMonth?: Date

  ISOWeek?: boolean
  fixedWeeks?: boolean

  buttonVariant?: string
}

interface CalendarProps extends CalendarBaseProps {
  mode?: "single" | "multiple" | "range"
  selected?: Date | Date[] | DateRange
  onSelect?: ((date: Date | undefined) => void) |
  ((dates: Date[] | undefined) => void) |
  ((range: DateRange | undefined) => void) |
  Dispatch<SetStateAction<Date[]>>
  required?: boolean
  max?: number
}

interface CalendarDayButtonProps {
  day: { date: Date }
  modifiers: { [key: string]: boolean }
  children: React.ReactNode
  onClick?: () => void
  onMouseEnter?: () => void
  onMouseLeave?: () => void
  disabled?: boolean
  className?: string
  style?: React.CSSProperties
  'data-date'?: string
  'data-modifiers'?: string
}

const months = [
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"
]

const shortMonths = [
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
]

const weekDays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]

const CalendarDayButton: React.FC<CalendarDayButtonProps> = ({
  day,
  modifiers,
  children,
  onClick,
  onMouseEnter,
  onMouseLeave,
  disabled,
  className,
  style,
  ...props
}) => {
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={onClick}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
      disabled={disabled}
      className={className}
      style={style}
      {...props}
    >
      {children}
    </Button>
  )
}

function Calendar(props: CalendarProps) {
  const {
    className,
    classNames = {},
    mode = "single",
    selected,
    onSelect,
    showOutsideDays = true,
    showWeekNumber = false,
    numberOfMonths = 1,
    captionLayout = "label",
    fromYear = new Date().getFullYear() - 100,
    toYear = new Date().getFullYear(),
    fromMonth,
    toMonth,
    yearOrder = "desc",
    disabled,
    modifiers = {},
    modifiersClassNames = {},
    locale = "en-US",
    weekStartsOn = 0,
    firstWeekContainsDate = 1,
    labels = {},
    formatters = {},
    onMonthChange,
    onDayClick,
    onDayMouseEnter,
    onDayMouseLeave,
    footer,
    components = {},
    animate = false,
    dir = "ltr",
    autoFocus = false,
    defaultMonth,
    ISOWeek = false,
    fixedWeeks = false,
    ...restProps
  } = props

  const [currentDates, setCurrentDates] = React.useState(() => {
    const baseDate = defaultMonth || (selected instanceof Date ? selected : new Date())
    return Array.from({ length: numberOfMonths }, (_, index) => {
      const date = new Date(baseDate)
      date.setMonth(date.getMonth() + index)
      return date
    })
  })

  const [showMonthPicker, setShowMonthPicker] = React.useState(false)
  const [showYearPicker, setShowYearPicker] = React.useState(false)
  const [hoveredDate, setHoveredDate] = React.useState<Date | null>(null)

  React.useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      const target = event.target as Element
      if (!target.closest('[data-calendar-container]')) {
        setShowMonthPicker(false)
        setShowYearPicker(false)
      }
    }

    if (showMonthPicker || showYearPicker) {
      document.addEventListener('mousedown', handleClickOutside)
      return () => document.removeEventListener('mousedown', handleClickOutside)
    }
  }, [showMonthPicker, showYearPicker])

  const getMonthNames = () => {
    if (formatters.formatMonthDropdown) {
      return shortMonths.map((_, index) => formatters.formatMonthDropdown!(new Date(2023, index, 1)))
    }
    return shortMonths
  }

  const getWeekDayNames = () => {
    const days = [...weekDays]
    for (let i = 0; i < weekStartsOn; i++) {
      days.push(days.shift()!)
    }
    return days
  }

  const getDaysInMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate()
  }

  const getFirstDayOfMonth = (year: number, month: number) => {
    const firstDay = new Date(year, month, 1).getDay()
    return (firstDay - weekStartsOn + 7) % 7
  }

  const getWeekNumber = (date: Date): number => {
    if (ISOWeek) {
      const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
      const dayNum = d.getUTCDay() || 7
      d.setUTCDate(d.getUTCDate() + 4 - dayNum)
      const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
      return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7)
    }

    const target = new Date(date.valueOf())
    const dayNr = (date.getDay() + 6) % 7
    target.setDate(target.getDate() - dayNr + 3)
    const jan4 = new Date(target.getFullYear(), 0, 4)
    const dayDiff = (target.getTime() - jan4.getTime()) / 86400000
    return 1 + Math.ceil(dayDiff / 7)
  }

  const isDateDisabled = (date: Date): boolean => {
    if (!disabled) return false

    if (Array.isArray(disabled)) {
      return disabled.some(d => d.toDateString() === date.toDateString())
    }

    if (typeof disabled === 'function') {
      return disabled(date)
    }

    if (typeof disabled === 'object') {
      if (disabled.before && date < disabled.before) return true
      if (disabled.after && date > disabled.after) return true
      if (disabled.dayOfWeek && disabled.dayOfWeek.includes(date.getDay())) return true
    }

    return false
  }

  const getDayModifiers = (date: Date, currentMonth: number, currentYear: number) => {
    const dayModifiers: { [key: string]: boolean } = {}

    dayModifiers.today = isToday(date)
    dayModifiers.selected = isSelected(date)
    dayModifiers.disabled = isDateDisabled(date)
    dayModifiers.outside = !isCurrentMonth(date, currentMonth, currentYear)
    dayModifiers.hovered = hoveredDate ? date.toDateString() === hoveredDate.toDateString() : false

    if (mode === 'range' && selected && typeof selected === 'object' && 'from' in selected) {
      const range = selected as DateRange
      if (range.from) {
        dayModifiers.range_start = date.toDateString() === range.from.toDateString()
      }
      if (range.to) {
        dayModifiers.range_end = date.toDateString() === range.to.toDateString()
      }
      if (range.from && range.to && date > range.from && date < range.to) {
        dayModifiers.range_middle = true
      }

      if (range.from && !range.to && hoveredDate) {
        const startDate = range.from < hoveredDate ? range.from : hoveredDate
        const endDate = range.from < hoveredDate ? hoveredDate : range.from

        if (date > startDate && date < endDate) {
          dayModifiers.range_middle_preview = true
        }
        if (date.toDateString() === endDate.toDateString()) {
          dayModifiers.range_end_preview = true
        }
      }
    }

    Object.entries(modifiers).forEach(([key, modifier]) => {
      if (Array.isArray(modifier)) {
        dayModifiers[key] = modifier.some(d => d.toDateString() === date.toDateString())
      } else if (typeof modifier === 'function') {
        dayModifiers[key] = modifier(date)
      } else if (typeof modifier === 'object') {
        let hasModifier = false
        if (modifier.dayOfWeek && modifier.dayOfWeek.includes(date.getDay())) {
          hasModifier = true
        }
        if (modifier.before && date < modifier.before) {
          hasModifier = true
        }
        if (modifier.after && date > modifier.after) {
          hasModifier = true
        }
        dayModifiers[key] = hasModifier
      }
    })

    return dayModifiers
  }

  const generateCalendarDays = (currentMonth: number, currentYear: number) => {
    const daysInMonth = getDaysInMonth(currentYear, currentMonth)
    const firstDay = getFirstDayOfMonth(currentYear, currentMonth)
    const days = []

    if (showOutsideDays || firstDay > 0) {
      const prevMonth = currentMonth === 0 ? 11 : currentMonth - 1
      const prevYear = currentMonth === 0 ? currentYear - 1 : currentYear
      const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth)

      for (let i = firstDay - 1; i >= 0; i--) {
        const date = new Date(prevYear, prevMonth, daysInPrevMonth - i)
        days.push({
          day: daysInPrevMonth - i,
          date,
          isCurrentMonth: false,
          weekNumber: showWeekNumber && i === firstDay - 1 ? getWeekNumber(date) : null
        })
      }
    }

    for (let day = 1; day <= daysInMonth; day++) {
      const date = new Date(currentYear, currentMonth, day)
      days.push({
        day,
        date,
        isCurrentMonth: true,
        weekNumber: showWeekNumber && date.getDay() === weekStartsOn ? getWeekNumber(date) : null
      })
    }

    const totalCells = fixedWeeks ? 42 : Math.ceil(days.length / 7) * 7
    const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1
    const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear

    for (let day = 1; days.length < totalCells; day++) {
      const date = new Date(nextYear, nextMonth, day)
      if (showOutsideDays) {
        days.push({
          day,
          date,
          isCurrentMonth: false,
          weekNumber: showWeekNumber && date.getDay() === weekStartsOn ? getWeekNumber(date) : null
        })
      }
    }

    return days
  }

  const isCurrentMonth = (date: Date, currentMonth: number, currentYear: number) => {
    return date.getMonth() === currentMonth && date.getFullYear() === currentYear
  }

  const isSelected = (date: Date): boolean => {
    if (!selected) return false

    if (mode === "single" && selected instanceof Date) {
      return date.toDateString() === selected.toDateString()
    }

    if (mode === "multiple" && Array.isArray(selected)) {
      return selected.some(d => d.toDateString() === date.toDateString())
    }

    if (mode === "range" && selected && typeof selected === 'object' && 'from' in selected) {
      const range = selected as DateRange
      if (!range.from) return false
      if (!range.to) return date.toDateString() === range.from.toDateString()
      return date >= range.from && date <= range.to
    }

    return false
  }

  const isToday = (date: Date) => {
    const today = new Date()
    return date.toDateString() === today.toDateString()
  }

  const handleDateClick = (date: Date) => {
    if (isDateDisabled(date)) return

    const modifiers = Object.keys(getDayModifiers(date, date.getMonth(), date.getFullYear())).filter(
      key => getDayModifiers(date, date.getMonth(), date.getFullYear())[key]
    )
    onDayClick?.(date, modifiers)

    if (mode === "single") {
      ; (onSelect as ((date: Date | undefined) => void) | undefined)?.(date)
    } else if (mode === "multiple") {
      const multipleOnSelect = onSelect as ((dates: Date[] | undefined) => void) | undefined
      const currentSelected = (selected as Date[]) || []
      const isAlreadySelected = currentSelected.some(d => d.toDateString() === date.toDateString())

      if (isAlreadySelected) {
        multipleOnSelect?.(currentSelected.filter(d => d.toDateString() !== date.toDateString()))
      } else {
        multipleOnSelect?.([...currentSelected, date])
      }
    } else if (mode === "range") {
      const rangeOnSelect = onSelect as ((range: DateRange | undefined) => void) | undefined
      const currentRange = (selected as DateRange) || {}

      if (!currentRange.from) {
        rangeOnSelect?.({ from: date, to: undefined })
      } else if (currentRange.from && currentRange.to) {
        if (date.getTime() === currentRange.from.getTime() || date.getTime() === currentRange.to.getTime()) {
          rangeOnSelect?.({ from: undefined, to: undefined })
        } else if (date >= currentRange.from && date <= currentRange.to) {
          rangeOnSelect?.({ from: currentRange.from, to: date })
        } else if (date < currentRange.from) {
          rangeOnSelect?.({ from: date, to: currentRange.to })
        } else {
          rangeOnSelect?.({ from: currentRange.from, to: date })
        }
      } else if (currentRange.from && !currentRange.to) {
        if (date < currentRange.from) {
          rangeOnSelect?.({ from: date, to: currentRange.from })
        } else if (date.getTime() === currentRange.from.getTime()) {
          rangeOnSelect?.({ from: undefined, to: undefined })
        } else {
          rangeOnSelect?.({ from: currentRange.from, to: date })
        }
      }
    }
  }

  const handleDateMouseEnter = (date: Date) => {
    const modifiers = Object.keys(getDayModifiers(date, date.getMonth(), date.getFullYear())).filter(
      key => getDayModifiers(date, date.getMonth(), date.getFullYear())[key]
    )
    setHoveredDate(date)
    onDayMouseEnter?.(date, modifiers)
  }

  const handleDateMouseLeave = (date: Date) => {
    const modifiers = Object.keys(getDayModifiers(date, date.getMonth(), date.getFullYear())).filter(
      key => getDayModifiers(date, date.getMonth(), date.getFullYear())[key]
    )
    setHoveredDate(null)
    onDayMouseLeave?.(date, modifiers)
  }

  const handleMonthSelect = (month: number, monthIndex: number = 0) => {
    const newDates = [...currentDates]
    newDates[monthIndex] = new Date(newDates[monthIndex].getFullYear(), month, 1)

    for (let i = monthIndex + 1; i < numberOfMonths; i++) {
      newDates[i] = new Date(newDates[i - 1])
      newDates[i].setMonth(newDates[i].getMonth() + 1)
    }

    setCurrentDates(newDates)
    setShowMonthPicker(false)
    onMonthChange?.(newDates[0])
  }

  const handleYearSelect = (year: number, monthIndex: number = 0) => {
    const newDates = [...currentDates]
    newDates[monthIndex] = new Date(year, newDates[monthIndex].getMonth(), 1)

    for (let i = monthIndex + 1; i < numberOfMonths; i++) {
      newDates[i] = new Date(newDates[i - 1])
      newDates[i].setMonth(newDates[i].getMonth() + 1)
    }

    setCurrentDates(newDates)
    setShowYearPicker(false)
    onMonthChange?.(newDates[0])
  }

  const navigateMonth = (direction: "prev" | "next") => {
    const newDates = currentDates.map(date => {
      const newDate = new Date(date)
      if (direction === "prev") {
        if (fromMonth && newDate <= fromMonth) return date
        newDate.setMonth(newDate.getMonth() - 1)
      } else {
        if (toMonth && newDate >= toMonth) return date
        newDate.setMonth(newDate.getMonth() + 1)
      }
      return newDate
    })
    setCurrentDates(newDates)
    onMonthChange?.(newDates[0])
  }

  const generateYears = () => {
    const years = []
    for (let year = fromYear; year <= toYear; year++) {
      years.push(year)
    }
    return yearOrder === "desc" ? years.reverse() : years
  }

  const weekDayNames = getWeekDayNames()
  const monthNames = getMonthNames()

  const renderCaption = (monthIndex: number) => {
    const currentDate = currentDates[monthIndex]
    const currentMonth = currentDate.getMonth()
    const currentYear = currentDate.getFullYear()

    const monthName = formatters.formatCaption
      ? formatters.formatCaption(currentDate)
      : `${months[currentMonth]} ${currentYear}`

    if (captionLayout === "label") {
      return (
        <div className="flex h-8 w-full items-center justify-center text-sm font-medium text-foreground">
          {monthName}
        </div>
      )
    }

    return (
      <div className="flex items-center gap-1">
        {(captionLayout === "dropdown" || captionLayout === "dropdown-months") && (
          <Button
            variant="outline"
            onClick={() => {
              setShowMonthPicker(!showMonthPicker)
              setShowYearPicker(false)
            }}
            className="h-8 px-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
          >
            {monthNames[currentMonth]}
            <ChevronDownIcon className="ml-1 h-3.5 w-3.5" />
          </Button>
        )}

        {captionLayout === "dropdown-years" && (
          <span className="text-sm font-medium text-foreground">{monthNames[currentMonth]}</span>
        )}

        {(captionLayout === "dropdown" || captionLayout === "dropdown-years") && (
          <Button
            variant="outline"
            onClick={() => {
              setShowYearPicker(!showYearPicker)
              setShowMonthPicker(false)
            }}
            className="h-8 px-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
          >
            {currentYear}
            <ChevronDownIcon className="ml-1 h-3.5 w-3.5" />
          </Button>
        )}

        {captionLayout === "dropdown-months" && (
          <span className="text-sm font-medium text-foreground">{currentYear}</span>
        )}


      </div>
    )
  }

  const CalendarDayButtonComponent = components.DayButton || CalendarDayButton

  return (
    <div
      className={cn(
    "bg-background text-foreground group/calendar p-3 border border-border rounded-md shadow-sm",
    animate && "transition-all",
    numberOfMonths > 1 && "flex flex-col gap-4 sm:flex-row",
    className
  )}
      dir={dir}
      data-calendar-container
      {...restProps}
    >
      {Array.from({ length: numberOfMonths }, (_, monthIndex) => {
        const currentDate = currentDates[monthIndex]
        const currentMonth = currentDate.getMonth()
        const currentYear = currentDate.getFullYear()
        const calendarDays = generateCalendarDays(currentMonth, currentYear)

        return (
          <div key={monthIndex} className="relative">
            <div className="flex items-center justify-between mb-4">
              <Button
                variant="ghost"
                size="icon"
                onClick={() => navigateMonth("prev")}
                className={cn(
                  "flex items-center justify-center h-8 w-8 hover:bg-accent hover:text-accent-foreground",
                  numberOfMonths === 1 && "flex",
                  numberOfMonths > 1 && monthIndex === 0 && "flex",
                  numberOfMonths > 1 && monthIndex > 0 && "hidden"
                )}
                aria-label={labels.labelPrevious?.() || "Previous month"}
                disabled={fromMonth && currentDate <= fromMonth}
              >
                <ChevronLeftIcon className="h-4 w-4" />
              </Button>

              {renderCaption(monthIndex)}

              <Button
                variant="ghost"
                size="icon"
                onClick={() => navigateMonth("next")}
                className={cn(
                  "flex items-center justify-center hover:bg-accent hover:text-accent-foreground",
                  numberOfMonths === 1 && "flex",
                  numberOfMonths > 1 && monthIndex === 0 && "flex sm:hidden",
                  numberOfMonths > 1 && monthIndex === numberOfMonths - 1 && "hidden sm:flex",
                  numberOfMonths > 1 && monthIndex > 0 && monthIndex < numberOfMonths - 1 && "hidden"
                )}
                aria-label={labels.labelNext?.() || "Next month"}
                disabled={toMonth && currentDate >= toMonth}
              >
                <ChevronRightIcon className="h-4 w-4" />
              </Button>
            </div>

            {showMonthPicker && (
              <div className="absolute top-10 bottom-0 left-0 right-0 z-40 bg-popover border border-border rounded-lg shadow-lg">
                <div className="p-4 h-full overflow-y-auto flex justify-center">
                  <div className="grid grid-cols-3 gap-3 w-full max-w-sm">
                    {months.map((month, index) => (
                      <Button
  key={month}
  variant="ghost"
  onClick={() => handleMonthSelect(index, monthIndex)}
  className={cn(
    "h-10 text-sm font-medium justify-center w-full min-w-0 hover:bg-accent hover:text-accent-foreground",
    index === currentMonth &&
      "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground"
  )}
>
  {month.slice(0, 3)}
</Button>

                    ))}
                  </div>
                </div>
              </div>
            )}

            {showYearPicker && (
              <div className="absolute top-10 bottom-0 left-0 right-0 z-40 bg-popover border border-border rounded-lg shadow-lg">
                <div className="p-4 h-full overflow-y-auto">
                  <div className="grid grid-cols-4 gap-2">
                    {generateYears().map((year) => (
                      <Button
  key={year}
  variant="ghost"
  onClick={() => handleYearSelect(year, monthIndex)}
  className={cn(
    "h-9 text-sm font-medium justify-center hover:bg-accent hover:text-accent-foreground w-full",
    year === currentYear &&
      "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground"
  )}
>
  {year}
</Button>

                    ))}
                  </div>
                </div>
              </div>
            )}

            <div className={cn("grid mb-2", showWeekNumber ? "grid-cols-8" : "grid-cols-7")}>
              {showWeekNumber && (
                <div className="flex items-center justify-center text-sm font-normal text-muted-foreground" style={{ height: 'var(--cell-size, 2rem)' }}>
                  #
                </div>
              )}
              {weekDayNames.map((day) => (
                <div
                  key={day}
                  className={cn(
                    "flex items-center justify-center text-xs font-normal text-muted-foreground",
                    "[height:var(--cell-size,2rem)] [min-height:var(--cell-size,2rem)]"
                  )}
                >
                  {formatters.formatWeekdayName
                    ? formatters.formatWeekdayName(new Date())
                    : day}
                </div>
              ))}
            </div>

            <div className={cn("grid gap-1", showWeekNumber ? "grid-cols-8" : "grid-cols-7")}>
              {calendarDays.map((dayObj, index) => {
                const modifiers = getDayModifiers(dayObj.date, currentMonth, currentYear)
                const modifiersArray = Object.keys(modifiers).filter(key => modifiers[key])
                const dayClassNames = modifiersArray.map(modifier => modifiersClassNames[modifier] || '').join(' ')

                const DayButtonComponent = CalendarDayButtonComponent

                return (
                  <React.Fragment key={`${monthIndex}-${index}`}>
                    {dayObj.weekNumber !== null && showWeekNumber && (
                      <div
                        className={cn(
                          "flex items-center justify-center text-xs text-muted-foreground",
                          "[height:var(--cell-size,2rem)] [width:var(--cell-size,2rem)]",
                          "[min-height:var(--cell-size,2rem)] [min-width:var(--cell-size,2rem)]"
                        )}
                      >
                        {formatters.formatWeekNumber
                          ? formatters.formatWeekNumber(dayObj.weekNumber)
                          : dayObj.weekNumber}
                      </div>
                    )}
                    <DayButtonComponent
                      day={{ date: dayObj.date }}
                      modifiers={modifiers}
                      onClick={() => handleDateClick(dayObj.date)}
                      onMouseEnter={() => handleDateMouseEnter(dayObj.date)}
                      onMouseLeave={() => handleDateMouseLeave(dayObj.date)}
                      disabled={modifiers.disabled}
                      className={cn(
                        "text-xs font-normal flex flex-col items-center justify-center p-0.5 relative gap-0.5",
                        "[height:var(--cell-size,2rem)] [width:var(--cell-size,2rem)]",
                        "[min-height:var(--cell-size,2rem)] [min-width:var(--cell-size,2rem)]",
                        "hover:bg-accent hover:text-accent-foreground",
                        !dayObj.isCurrentMonth && showOutsideDays && "text-muted-foreground opacity-40",
                        !dayObj.isCurrentMonth && !showOutsideDays && "invisible",
                        modifiers.selected && "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
                        modifiers.today && !modifiers.selected && "bg-accent text-accent-foreground font-semibold",
                        modifiers.range_start && "bg-primary text-primary-foreground rounded-l-md rounded-r-none",
                        modifiers.range_end && "bg-primary text-primary-foreground rounded-r-md rounded-l-none",
                        modifiers.range_middle && "bg-accent text-accent-foreground rounded-none",
                        modifiers.range_middle_preview && "bg-muted text-muted-foreground rounded-none opacity-50",
                        modifiers.range_end_preview && "bg-primary/50 text-primary-foreground rounded-r-md rounded-l-none opacity-50",
                        modifiers.disabled && "text-muted-foreground opacity-50 cursor-not-allowed hover:bg-transparent",
                        dayClassNames,
                        classNames.day
                      )}
                      data-date={dayObj.date.toISOString()}
                      data-modifiers={modifiersArray.join(" ")}
                    >
                      {formatters.formatDay ? formatters.formatDay(dayObj.date) : dayObj.day}
                    </DayButtonComponent>
                  </React.Fragment>
                )
              })}
            </div>

            {footer && (
              <div className="mt-4 text-center text-sm text-muted-foreground">
                {footer}
              </div>
            )}
          </div>
        )
      })}
    </div>
  )
}

export { Calendar, CalendarDayButton }

Installation

npx shadcn@latest add @scrollxui/calendar

Usage

import { Calendar } from "@/components/calendar"
<Calendar />