1"use client";
2
3import { DayPicker } from "@daypicker/react";
4import { ChevronLeftIcon, ChevronRightIcon, ChevronsUpDownIcon } from "lucide-react";
5import type * as React from "react";
6import { cn } from "~/lib/utils";
7
8const buttonClassNames =
9 "relative flex size-(--cell-size) text-base sm:text-sm items-center justify-center rounded-lg text-foreground not-in-data-selected:hover:bg-accent disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0";
10
11const defaultComponents = {
12 Chevron: ({
13 className,
14 orientation,
15 ...props
16 }: {
17 className?: string;
18 orientation?: "left" | "right" | "up" | "down";
19 }): React.ReactElement => {
20 if (orientation === "left") {
21 return (
22 <ChevronLeftIcon
23 className={cn(className, "rtl:rotate-180")}
24 {...props}
25 aria-hidden="true"
26 />
27 );
28 }
29
30 if (orientation === "right") {
31 return (
32 <ChevronRightIcon
33 className={cn(className, "rtl:rotate-180")}
34 {...props}
35 aria-hidden="true"
36 />
37 );
38 }
39
40 return <ChevronsUpDownIcon className={className} {...props} aria-hidden="true" />;
41 },
42};
43
44export function Calendar({
45 className,
46 classNames,
47 showOutsideDays = true,
48 components: userComponents,
49 mode = "single",
50 ...props
51}: React.ComponentProps<typeof DayPicker>): React.ReactElement {
52 const defaultClassNames = {
53 button_next: buttonClassNames,
54 button_previous: buttonClassNames,
55 caption_label: "text-base sm:text-sm font-medium flex items-center gap-2 h-full",
56 day: "size-(--cell-size) text-sm py-px",
57 day_button: cn(
58 buttonClassNames,
59 "in-data-disabled:pointer-events-none in-[.range-middle]:rounded-none in-[.range-end:not(.range-start)]:rounded-s-none in-[.range-start:not(.range-end)]:rounded-e-none in-[.range-middle]:in-data-selected:bg-accent in-data-selected:bg-primary in-[.range-middle]:in-data-selected:text-foreground in-data-disabled:text-muted-foreground/72 in-data-outside:text-muted-foreground/72 in-data-selected:in-data-outside:text-primary-foreground in-data-selected:text-primary-foreground in-data-disabled:line-through outline-none in-[[data-selected]:not(.range-middle)]:transition-[border-radius,box-shadow] focus-visible:z-1 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50",
60 ),
61 dropdown: "absolute bg-popover inset-0 opacity-0",
62 dropdown_root:
63 "relative has-focus:border-ring has-focus:ring-ring/50 has-focus:ring-[3px] has-focus:ring-inset border border-input shadow-xs/5 rounded-lg px-[calc(--spacing(3)-1px)] h-9 sm:h-8 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:-me-1",
64 dropdowns:
65 "w-full flex items-center text-base sm:text-sm justify-center h-(--cell-size) gap-1.5 *:[span]:font-medium",
66 hidden: "invisible",
67 month: "w-full",
68 month_caption:
69 "relative mx-(--cell-size) px-1 mb-1 flex h-(--cell-size) items-center justify-center z-2",
70 months: "relative flex flex-col sm:flex-row gap-2",
71 nav: "absolute top-0 flex w-full justify-between z-1",
72 outside: "text-muted-foreground data-selected:bg-accent/50 data-selected:text-muted-foreground",
73 range_end: "range-end",
74 range_middle: "range-middle",
75 range_start: "range-start",
76 today:
77 "*:after:pointer-events-none *:after:absolute *:after:bottom-1 *:after:start-1/2 *:after:z-1 *:after:size-[3px] *:after:-translate-x-1/2 *:after:rounded-full *:after:bg-primary [&[data-selected]:not(.range-middle)>*]:after:bg-background [&[data-disabled]>*]:after:bg-foreground/30",
78 week_number: "size-(--cell-size) p-0 text-xs font-medium text-muted-foreground/72",
79 weekday: "size-(--cell-size) p-0 text-xs font-medium text-muted-foreground/72",
80 };
81 const mergedClassNames: typeof defaultClassNames = Object.keys(defaultClassNames).reduce(
82 (acc, key) => {
83 const userClass = classNames?.[key as keyof typeof classNames];
84 const baseClass = defaultClassNames[key as keyof typeof defaultClassNames];
85
86 acc[key as keyof typeof defaultClassNames] = userClass ? cn(baseClass, userClass) : baseClass;
87
88 return acc;
89 },
90 { ...defaultClassNames } as typeof defaultClassNames,
91 );
92
93 const mergedComponents = {
94 ...defaultComponents,
95 ...userComponents,
96 };
97
98 const dayPickerProps = {
99 className: cn("w-fit [--cell-size:--spacing(10)] sm:[--cell-size:--spacing(9)]", className),
100 classNames: mergedClassNames,
101 components: mergedComponents,
102 "data-slot": "calendar",
103 formatters: {
104 formatMonthDropdown: (date: Date) => date.toLocaleString("default", { month: "short" }),
105 } as React.ComponentProps<typeof DayPicker>["formatters"],
106 mode,
107 showOutsideDays,
108 ...props,
109 };
110
111 return <DayPicker {...(dayPickerProps as React.ComponentProps<typeof DayPicker>)} />;
112}