Calendar
Feature calendar grid component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring drag-and-drop, date selection, and internationalization.
'use client';import { faker } from '@faker-js/faker';import { CalendarBody, CalendarDate, CalendarDatePagination, CalendarDatePicker, CalendarHeader, CalendarItem, CalendarMonthPicker, CalendarProvider, CalendarYearPicker,} from '@/components/ui/shadcn-io/calendar';const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);const statuses = [ { id: faker.string.uuid(), name: 'Planned', color: '#6B7280' }, { id: faker.string.uuid(), name: 'In Progress', color: '#F59E0B' }, { id: faker.string.uuid(), name: 'Done', color: '#10B981' },];const exampleFeatures = Array.from({ length: 20 }) .fill(null) .map(() => ({ id: faker.string.uuid(), name: capitalize(faker.company.buzzPhrase()), startAt: faker.date.past({ years: 0.5, refDate: new Date() }), endAt: faker.date.future({ years: 0.5, refDate: new Date() }), status: faker.helpers.arrayElement(statuses), }));const earliestYear = exampleFeatures .map((feature) => feature.startAt.getFullYear()) .sort() .at(0) ?? new Date().getFullYear();const latestYear = exampleFeatures .map((feature) => feature.endAt.getFullYear()) .sort() .at(-1) ?? new Date().getFullYear();const Example = () => ( <CalendarProvider> <CalendarDate> <CalendarDatePicker> <CalendarMonthPicker /> <CalendarYearPicker end={latestYear} start={earliestYear} /> </CalendarDatePicker> <CalendarDatePagination /> </CalendarDate> <CalendarHeader /> <CalendarBody features={exampleFeatures}> {({ feature }) => <CalendarItem feature={feature} key={feature.id} />} </CalendarBody> </CalendarProvider>);export default Example;
'use client';import { getDay, getDaysInMonth, isSameDay } from 'date-fns';import { atom, useAtom } from 'jotai';import { Check, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDown,} from 'lucide-react';import { createContext, memo, type ReactNode, useCallback, useContext, useMemo, useState,} from 'react';import { Button } from '@/components/ui/button';import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from '@/components/ui/command';import { Popover, PopoverContent, PopoverTrigger,} from '@/components/ui/popover';import { cn } from '@/lib/utils';export type CalendarState = { month: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; year: number;};const monthAtom = atom<CalendarState['month']>( new Date().getMonth() as CalendarState['month']);const yearAtom = atom<CalendarState['year']>(new Date().getFullYear());export const useCalendarMonth = () => useAtom(monthAtom);export const useCalendarYear = () => useAtom(yearAtom);type CalendarContextProps = { locale: Intl.LocalesArgument; startDay: number;};const CalendarContext = createContext<CalendarContextProps>({ locale: 'en-US', startDay: 0,});export type Status = { id: string; name: string; color: string;};export type Feature = { id: string; name: string; startAt: Date; endAt: Date; status: Status;};type ComboboxProps = { value: string; setValue: (value: string) => void; data: { value: string; label: string; }[]; labels: { button: string; empty: string; search: string; }; className?: string;};export const monthsForLocale = ( localeName: Intl.LocalesArgument, monthFormat: Intl.DateTimeFormatOptions['month'] = 'long') => { const format = new Intl.DateTimeFormat(localeName, { month: monthFormat }) .format; return [...new Array(12).keys()].map((m) => format(new Date(Date.UTC(2021, m, 2))) );};export const daysForLocale = ( locale: Intl.LocalesArgument, startDay: number) => { const weekdays: string[] = []; const baseDate = new Date(2024, 0, startDay); for (let i = 0; i < 7; i++) { weekdays.push( new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(baseDate) ); baseDate.setDate(baseDate.getDate() + 1); } return weekdays;};const Combobox = ({ value, setValue, data, labels, className,}: ComboboxProps) => { const [open, setOpen] = useState(false); return ( <Popover onOpenChange={setOpen} open={open}> <PopoverTrigger asChild> <Button aria-expanded={open} className={cn('w-40 justify-between capitalize', className)} variant="outline" > {value ? data.find((item) => item.value === value)?.label : labels.button} <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-40 p-0"> <Command filter={(value, search) => { const label = data.find((item) => item.value === value)?.label; return label?.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; }} > <CommandInput placeholder={labels.search} /> <CommandList> <CommandEmpty>{labels.empty}</CommandEmpty> <CommandGroup> {data.map((item) => ( <CommandItem className="capitalize" key={item.value} onSelect={(currentValue) => { setValue(currentValue === value ? '' : currentValue); setOpen(false); }} value={item.value} > <Check className={cn( 'mr-2 h-4 w-4', value === item.value ? 'opacity-100' : 'opacity-0' )} /> {item.label} </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> );};type OutOfBoundsDayProps = { day: number;};const OutOfBoundsDay = ({ day }: OutOfBoundsDayProps) => ( <div className="relative h-full w-full bg-secondary p-1 text-muted-foreground text-xs"> {day} </div>);export type CalendarBodyProps = { features: Feature[]; children: (props: { feature: Feature }) => ReactNode;};export const CalendarBody = ({ features, children }: CalendarBodyProps) => { const [month] = useCalendarMonth(); const [year] = useCalendarYear(); const { startDay } = useContext(CalendarContext); // Memoize expensive date calculations const currentMonthDate = useMemo( () => new Date(year, month, 1), [year, month] ); const daysInMonth = useMemo( () => getDaysInMonth(currentMonthDate), [currentMonthDate] ); const firstDay = useMemo( () => (getDay(currentMonthDate) - startDay + 7) % 7, [currentMonthDate, startDay] ); // Memoize previous month calculations const prevMonthData = useMemo(() => { const prevMonth = month === 0 ? 11 : month - 1; const prevMonthYear = month === 0 ? year - 1 : year; const prevMonthDays = getDaysInMonth(new Date(prevMonthYear, prevMonth, 1)); const prevMonthDaysArray = Array.from( { length: prevMonthDays }, (_, i) => i + 1 ); return { prevMonthDays, prevMonthDaysArray }; }, [month, year]); // Memoize next month calculations const nextMonthData = useMemo(() => { const nextMonth = month === 11 ? 0 : month + 1; const nextMonthYear = month === 11 ? year + 1 : year; const nextMonthDays = getDaysInMonth(new Date(nextMonthYear, nextMonth, 1)); const nextMonthDaysArray = Array.from( { length: nextMonthDays }, (_, i) => i + 1 ); return { nextMonthDaysArray }; }, [month, year]); // Memoize features filtering by day to avoid recalculating on every render const featuresByDay = useMemo(() => { const result: { [day: number]: Feature[] } = {}; for (let day = 1; day <= daysInMonth; day++) { result[day] = features.filter((feature) => { return isSameDay(new Date(feature.endAt), new Date(year, month, day)); }); } return result; }, [features, daysInMonth, year, month]); const days: ReactNode[] = []; for (let i = 0; i < firstDay; i++) { const day = prevMonthData.prevMonthDaysArray[ prevMonthData.prevMonthDays - firstDay + i ]; if (day) { days.push(<OutOfBoundsDay day={day} key={`prev-${i}`} />); } } for (let day = 1; day <= daysInMonth; day++) { const featuresForDay = featuresByDay[day] || []; days.push( <div className="relative flex h-full w-full flex-col gap-1 p-1 text-muted-foreground text-xs" key={day} > {day} <div> {featuresForDay.slice(0, 3).map((feature) => children({ feature }))} </div> {featuresForDay.length > 3 && ( <span className="block text-muted-foreground text-xs"> +{featuresForDay.length - 3} more </span> )} </div> ); } const remainingDays = 7 - ((firstDay + daysInMonth) % 7); if (remainingDays < 7) { for (let i = 0; i < remainingDays; i++) { const day = nextMonthData.nextMonthDaysArray[i]; if (day) { days.push(<OutOfBoundsDay day={day} key={`next-${i}`} />); } } } return ( <div className="grid flex-grow grid-cols-7"> {days.map((day, index) => ( <div className={cn( 'relative aspect-square overflow-hidden border-t border-r', index % 7 === 6 && 'border-r-0' )} key={index} > {day} </div> ))} </div> );};export type CalendarDatePickerProps = { className?: string; children: ReactNode;};export const CalendarDatePicker = ({ className, children,}: CalendarDatePickerProps) => ( <div className={cn('flex items-center gap-1', className)}>{children}</div>);export type CalendarMonthPickerProps = { className?: string;};export const CalendarMonthPicker = ({ className,}: CalendarMonthPickerProps) => { const [month, setMonth] = useCalendarMonth(); const { locale } = useContext(CalendarContext); // Memoize month data to avoid recalculating date formatting const monthData = useMemo(() => { return monthsForLocale(locale).map((month, index) => ({ value: index.toString(), label: month, })); }, [locale]); return ( <Combobox className={className} data={monthData} labels={{ button: 'Select month', empty: 'No month found', search: 'Search month', }} setValue={(value) => setMonth(Number.parseInt(value) as CalendarState['month']) } value={month.toString()} /> );};export type CalendarYearPickerProps = { className?: string; start: number; end: number;};export const CalendarYearPicker = ({ className, start, end,}: CalendarYearPickerProps) => { const [year, setYear] = useCalendarYear(); return ( <Combobox className={className} data={Array.from({ length: end - start + 1 }, (_, i) => ({ value: (start + i).toString(), label: (start + i).toString(), }))} labels={{ button: 'Select year', empty: 'No year found', search: 'Search year', }} setValue={(value) => setYear(Number.parseInt(value))} value={year.toString()} /> );};export type CalendarDatePaginationProps = { className?: string;};export const CalendarDatePagination = ({ className,}: CalendarDatePaginationProps) => { const [month, setMonth] = useCalendarMonth(); const [year, setYear] = useCalendarYear(); const handlePreviousMonth = useCallback(() => { if (month === 0) { setMonth(11); setYear(year - 1); } else { setMonth((month - 1) as CalendarState['month']); } }, [month, year, setMonth, setYear]); const handleNextMonth = useCallback(() => { if (month === 11) { setMonth(0); setYear(year + 1); } else { setMonth((month + 1) as CalendarState['month']); } }, [month, year, setMonth, setYear]); return ( <div className={cn('flex items-center gap-2', className)}> <Button onClick={handlePreviousMonth} size="icon" variant="ghost"> <ChevronLeftIcon size={16} /> </Button> <Button onClick={handleNextMonth} size="icon" variant="ghost"> <ChevronRightIcon size={16} /> </Button> </div> );};export type CalendarDateProps = { children: ReactNode;};export const CalendarDate = ({ children }: CalendarDateProps) => ( <div className="flex items-center justify-between p-3">{children}</div>);export type CalendarHeaderProps = { className?: string;};export const CalendarHeader = ({ className }: CalendarHeaderProps) => { const { locale, startDay } = useContext(CalendarContext); // Memoize days data to avoid recalculating date formatting const daysData = useMemo(() => { return daysForLocale(locale, startDay); }, [locale, startDay]); return ( <div className={cn('grid flex-grow grid-cols-7', className)}> {daysData.map((day) => ( <div className="p-3 text-right text-muted-foreground text-xs" key={day}> {day} </div> ))} </div> );};export type CalendarItemProps = { feature: Feature; className?: string;};export const CalendarItem = memo( ({ feature, className }: CalendarItemProps) => ( <div className={cn('flex items-center gap-2', className)}> <div className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: feature.status.color, }} /> <span className="truncate">{feature.name}</span> </div> ));CalendarItem.displayName = 'CalendarItem';export type CalendarProviderProps = { locale?: Intl.LocalesArgument; startDay?: number; children: ReactNode; className?: string;};export const CalendarProvider = ({ locale = 'en-US', startDay = 0, children, className,}: CalendarProviderProps) => ( <CalendarContext.Provider value={{ locale, startDay }}> <div className={cn('relative flex flex-col', className)}>{children}</div> </CalendarContext.Provider>);
Installation
npx shadcn@latest add https://www.shadcn.io/registry/calendar.json
npx shadcn@latest add https://www.shadcn.io/registry/calendar.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/calendar.json
bunx shadcn@latest add https://www.shadcn.io/registry/calendar.json
Features
- Feature grouping - Day-based organization with color-coded status indicators using JavaScript sorting
- Drag-and-drop - Interactive feature movement with dnd kit integration for React applications
- Date selection - Range picker with pagination controls using date-fns library
- Smart truncation - Automatic overflow handling for grid cells using TypeScript logic
- Internationalization - Locale support with customizable formatting for Next.js projects
- State management - Jotai integration for efficient calendar state using React Context
- Icon integration - Lucide icons with consistent styling using shadcn/ui theming
- Open source - Free calendar component with grid layout and Tailwind CSS styling
Examples
Without a date picker
'use client';import { faker } from '@faker-js/faker';import { CalendarBody, CalendarHeader, CalendarItem, CalendarProvider,} from '@/components/ui/shadcn-io/calendar';const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);const statuses = [ { id: faker.string.uuid(), name: 'Planned', color: '#6B7280' }, { id: faker.string.uuid(), name: 'In Progress', color: '#F59E0B' }, { id: faker.string.uuid(), name: 'Done', color: '#10B981' },];const exampleFeatures = Array.from({ length: 20 }) .fill(null) .map(() => ({ id: faker.string.uuid(), name: capitalize(faker.company.buzzPhrase()), startAt: faker.date.past({ years: 0.5, refDate: new Date() }), endAt: faker.date.future({ years: 0.5, refDate: new Date() }), status: faker.helpers.arrayElement(statuses), }));const Example = () => ( <CalendarProvider> <CalendarHeader /> <CalendarBody features={exampleFeatures}> {({ feature }) => <CalendarItem feature={feature} key={feature.id} />} </CalendarBody> </CalendarProvider>);export default Example;
'use client';import { getDay, getDaysInMonth, isSameDay } from 'date-fns';import { atom, useAtom } from 'jotai';import { Check, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDown,} from 'lucide-react';import { createContext, memo, type ReactNode, useCallback, useContext, useMemo, useState,} from 'react';import { Button } from '@/components/ui/button';import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from '@/components/ui/command';import { Popover, PopoverContent, PopoverTrigger,} from '@/components/ui/popover';import { cn } from '@/lib/utils';export type CalendarState = { month: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; year: number;};const monthAtom = atom<CalendarState['month']>( new Date().getMonth() as CalendarState['month']);const yearAtom = atom<CalendarState['year']>(new Date().getFullYear());export const useCalendarMonth = () => useAtom(monthAtom);export const useCalendarYear = () => useAtom(yearAtom);type CalendarContextProps = { locale: Intl.LocalesArgument; startDay: number;};const CalendarContext = createContext<CalendarContextProps>({ locale: 'en-US', startDay: 0,});export type Status = { id: string; name: string; color: string;};export type Feature = { id: string; name: string; startAt: Date; endAt: Date; status: Status;};type ComboboxProps = { value: string; setValue: (value: string) => void; data: { value: string; label: string; }[]; labels: { button: string; empty: string; search: string; }; className?: string;};export const monthsForLocale = ( localeName: Intl.LocalesArgument, monthFormat: Intl.DateTimeFormatOptions['month'] = 'long') => { const format = new Intl.DateTimeFormat(localeName, { month: monthFormat }) .format; return [...new Array(12).keys()].map((m) => format(new Date(Date.UTC(2021, m, 2))) );};export const daysForLocale = ( locale: Intl.LocalesArgument, startDay: number) => { const weekdays: string[] = []; const baseDate = new Date(2024, 0, startDay); for (let i = 0; i < 7; i++) { weekdays.push( new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(baseDate) ); baseDate.setDate(baseDate.getDate() + 1); } return weekdays;};const Combobox = ({ value, setValue, data, labels, className,}: ComboboxProps) => { const [open, setOpen] = useState(false); return ( <Popover onOpenChange={setOpen} open={open}> <PopoverTrigger asChild> <Button aria-expanded={open} className={cn('w-40 justify-between capitalize', className)} variant="outline" > {value ? data.find((item) => item.value === value)?.label : labels.button} <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-40 p-0"> <Command filter={(value, search) => { const label = data.find((item) => item.value === value)?.label; return label?.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; }} > <CommandInput placeholder={labels.search} /> <CommandList> <CommandEmpty>{labels.empty}</CommandEmpty> <CommandGroup> {data.map((item) => ( <CommandItem className="capitalize" key={item.value} onSelect={(currentValue) => { setValue(currentValue === value ? '' : currentValue); setOpen(false); }} value={item.value} > <Check className={cn( 'mr-2 h-4 w-4', value === item.value ? 'opacity-100' : 'opacity-0' )} /> {item.label} </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> );};type OutOfBoundsDayProps = { day: number;};const OutOfBoundsDay = ({ day }: OutOfBoundsDayProps) => ( <div className="relative h-full w-full bg-secondary p-1 text-muted-foreground text-xs"> {day} </div>);export type CalendarBodyProps = { features: Feature[]; children: (props: { feature: Feature }) => ReactNode;};export const CalendarBody = ({ features, children }: CalendarBodyProps) => { const [month] = useCalendarMonth(); const [year] = useCalendarYear(); const { startDay } = useContext(CalendarContext); // Memoize expensive date calculations const currentMonthDate = useMemo( () => new Date(year, month, 1), [year, month] ); const daysInMonth = useMemo( () => getDaysInMonth(currentMonthDate), [currentMonthDate] ); const firstDay = useMemo( () => (getDay(currentMonthDate) - startDay + 7) % 7, [currentMonthDate, startDay] ); // Memoize previous month calculations const prevMonthData = useMemo(() => { const prevMonth = month === 0 ? 11 : month - 1; const prevMonthYear = month === 0 ? year - 1 : year; const prevMonthDays = getDaysInMonth(new Date(prevMonthYear, prevMonth, 1)); const prevMonthDaysArray = Array.from( { length: prevMonthDays }, (_, i) => i + 1 ); return { prevMonthDays, prevMonthDaysArray }; }, [month, year]); // Memoize next month calculations const nextMonthData = useMemo(() => { const nextMonth = month === 11 ? 0 : month + 1; const nextMonthYear = month === 11 ? year + 1 : year; const nextMonthDays = getDaysInMonth(new Date(nextMonthYear, nextMonth, 1)); const nextMonthDaysArray = Array.from( { length: nextMonthDays }, (_, i) => i + 1 ); return { nextMonthDaysArray }; }, [month, year]); // Memoize features filtering by day to avoid recalculating on every render const featuresByDay = useMemo(() => { const result: { [day: number]: Feature[] } = {}; for (let day = 1; day <= daysInMonth; day++) { result[day] = features.filter((feature) => { return isSameDay(new Date(feature.endAt), new Date(year, month, day)); }); } return result; }, [features, daysInMonth, year, month]); const days: ReactNode[] = []; for (let i = 0; i < firstDay; i++) { const day = prevMonthData.prevMonthDaysArray[ prevMonthData.prevMonthDays - firstDay + i ]; if (day) { days.push(<OutOfBoundsDay day={day} key={`prev-${i}`} />); } } for (let day = 1; day <= daysInMonth; day++) { const featuresForDay = featuresByDay[day] || []; days.push( <div className="relative flex h-full w-full flex-col gap-1 p-1 text-muted-foreground text-xs" key={day} > {day} <div> {featuresForDay.slice(0, 3).map((feature) => children({ feature }))} </div> {featuresForDay.length > 3 && ( <span className="block text-muted-foreground text-xs"> +{featuresForDay.length - 3} more </span> )} </div> ); } const remainingDays = 7 - ((firstDay + daysInMonth) % 7); if (remainingDays < 7) { for (let i = 0; i < remainingDays; i++) { const day = nextMonthData.nextMonthDaysArray[i]; if (day) { days.push(<OutOfBoundsDay day={day} key={`next-${i}`} />); } } } return ( <div className="grid flex-grow grid-cols-7"> {days.map((day, index) => ( <div className={cn( 'relative aspect-square overflow-hidden border-t border-r', index % 7 === 6 && 'border-r-0' )} key={index} > {day} </div> ))} </div> );};export type CalendarDatePickerProps = { className?: string; children: ReactNode;};export const CalendarDatePicker = ({ className, children,}: CalendarDatePickerProps) => ( <div className={cn('flex items-center gap-1', className)}>{children}</div>);export type CalendarMonthPickerProps = { className?: string;};export const CalendarMonthPicker = ({ className,}: CalendarMonthPickerProps) => { const [month, setMonth] = useCalendarMonth(); const { locale } = useContext(CalendarContext); // Memoize month data to avoid recalculating date formatting const monthData = useMemo(() => { return monthsForLocale(locale).map((month, index) => ({ value: index.toString(), label: month, })); }, [locale]); return ( <Combobox className={className} data={monthData} labels={{ button: 'Select month', empty: 'No month found', search: 'Search month', }} setValue={(value) => setMonth(Number.parseInt(value) as CalendarState['month']) } value={month.toString()} /> );};export type CalendarYearPickerProps = { className?: string; start: number; end: number;};export const CalendarYearPicker = ({ className, start, end,}: CalendarYearPickerProps) => { const [year, setYear] = useCalendarYear(); return ( <Combobox className={className} data={Array.from({ length: end - start + 1 }, (_, i) => ({ value: (start + i).toString(), label: (start + i).toString(), }))} labels={{ button: 'Select year', empty: 'No year found', search: 'Search year', }} setValue={(value) => setYear(Number.parseInt(value))} value={year.toString()} /> );};export type CalendarDatePaginationProps = { className?: string;};export const CalendarDatePagination = ({ className,}: CalendarDatePaginationProps) => { const [month, setMonth] = useCalendarMonth(); const [year, setYear] = useCalendarYear(); const handlePreviousMonth = useCallback(() => { if (month === 0) { setMonth(11); setYear(year - 1); } else { setMonth((month - 1) as CalendarState['month']); } }, [month, year, setMonth, setYear]); const handleNextMonth = useCallback(() => { if (month === 11) { setMonth(0); setYear(year + 1); } else { setMonth((month + 1) as CalendarState['month']); } }, [month, year, setMonth, setYear]); return ( <div className={cn('flex items-center gap-2', className)}> <Button onClick={handlePreviousMonth} size="icon" variant="ghost"> <ChevronLeftIcon size={16} /> </Button> <Button onClick={handleNextMonth} size="icon" variant="ghost"> <ChevronRightIcon size={16} /> </Button> </div> );};export type CalendarDateProps = { children: ReactNode;};export const CalendarDate = ({ children }: CalendarDateProps) => ( <div className="flex items-center justify-between p-3">{children}</div>);export type CalendarHeaderProps = { className?: string;};export const CalendarHeader = ({ className }: CalendarHeaderProps) => { const { locale, startDay } = useContext(CalendarContext); // Memoize days data to avoid recalculating date formatting const daysData = useMemo(() => { return daysForLocale(locale, startDay); }, [locale, startDay]); return ( <div className={cn('grid flex-grow grid-cols-7', className)}> {daysData.map((day) => ( <div className="p-3 text-right text-muted-foreground text-xs" key={day}> {day} </div> ))} </div> );};export type CalendarItemProps = { feature: Feature; className?: string;};export const CalendarItem = memo( ({ feature, className }: CalendarItemProps) => ( <div className={cn('flex items-center gap-2', className)}> <div className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: feature.status.color, }} /> <span className="truncate">{feature.name}</span> </div> ));CalendarItem.displayName = 'CalendarItem';export type CalendarProviderProps = { locale?: Intl.LocalesArgument; startDay?: number; children: ReactNode; className?: string;};export const CalendarProvider = ({ locale = 'en-US', startDay = 0, children, className,}: CalendarProviderProps) => ( <CalendarContext.Provider value={{ locale, startDay }}> <div className={cn('relative flex flex-col', className)}>{children}</div> </CalendarContext.Provider>);
Use Cases
- Project management - Feature roadmaps and release planning interfaces
- Event scheduling - Meeting and appointment calendar systems
- Content planning - Editorial calendars with status tracking
- Team coordination - Task assignment and deadline management
Implementation
Built with dnd kit for drag functionality. Uses Jotai for state management and date-fns for date operations. Supports internationalization with locale props. Grid layout with responsive design.
Writing Text
Animated text component that reveals words sequentially with typing effect. Perfect for React applications requiring progressive text display with Next.js integration and TypeScript support.
Mini Calendar
Compact date picker component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring horizontal layout, navigation controls, and accessibility features.