Pick Close Day - a tiny calendar

PreviousNext
Docs
react-marketcomponent

Preview

Loading preview…
pick-close-day.tsx
'use client'

import { cn } from '@/lib/utils'
import { addDays, differenceInCalendarDays } from 'date-fns'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import React from 'react'
import { motion as m, AnimatePresence } from 'motion/react'

interface PickCloseDayProps extends React.HTMLAttributes<HTMLDivElement> {
    /** Initial or controlled date (defaults to today) */
    date?: Date
    /** Fires whenever the date changes */
    onDateChange?: (date: Date) => void
}

export function PickCloseDay({
    date: controlledDate,
    onDateChange,
    className,
    ...rest
}: PickCloseDayProps) {
    // Uncontrolled fallback
    const [internalDate, setInternalDate] = React.useState(new Date())
    const date = controlledDate ?? internalDate

    // Compute slide direction based on previous date
    const prevDateRef = React.useRef(date)
    const direction =
        date.getTime() - prevDateRef.current.getTime() > 0 ? 1 : -1
    React.useEffect(() => {
        prevDateRef.current = date
    }, [date])

    // Variants for sliding transition
    const variants = {
        enter: (direction: number) => ({
            x: direction > 0 ? 20 : -20,
            opacity: 0,
        }),
        center: { x: 0, opacity: 1 },
        exit: (direction: number) => ({
            x: direction > 0 ? -20 : 20,
            opacity: 0,
        }),
    }

    const label = React.useMemo(() => {
        const delta = differenceInCalendarDays(date, new Date())
        if (delta === 0) return 'Today'
        if (delta === -1) return 'Yesterday'
        if (delta === 1) return 'Tomorrow'
        if (delta < 0) return `${Math.abs(delta)} days ago`
        return `In ${delta} days`
    }, [date])

    const shift = (by: number) => {
        const next = addDays(date, by)
        if (!controlledDate) setInternalDate(next)
        onDateChange?.(next)
    }

    return (
        <div
            {...rest}
            className={cn(
                'inline-flex items-center gap-2 select-none',
                'bg-accent text-accent-foreground rounded-md px-3 py-1.5',
                className
            )}
        >
            <button
                type="button"
                aria-label="Previous day"
                onClick={() => shift(-1)}
            >
                <ChevronLeft className="h-4 w-4" />
            </button>

            <AnimatePresence mode="wait">
                <m.span
                    key={date.toISOString()}
                    custom={direction}
                    variants={variants}
                    initial="enter"
                    animate="center"
                    exit="exit"
                    transition={{ duration: 0.3 }}
                    style={{ display: 'inline-block' }}
                >
                    {label}
                </m.span>
            </AnimatePresence>

            <button
                type="button"
                aria-label="Next day"
                onClick={() => shift(1)}
            >
                <ChevronRight className="h-4 w-4" />
            </button>
        </div>
    )
}

Installation

npx shadcn@latest add @react-market/pick-close-day

Usage

import { PickCloseDay } from "@/components/pick-close-day"
<PickCloseDay />