{"version":3,"file":"Scheduler.cjs","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the grid is defined by\n * anchor, days, startHour, endHour and snapMinutes; the content by events,\n * renderEvent, onEventClick and onSlotClick; the reading by locale, showCurrentTime\n * and now. The body lays out overlapping events into columns, which needs the whole\n * day's events at once.\n */\nimport { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n    dayRange,\n    type DayWindow,\n    fractionOfWindow,\n    hourMarks,\n    isSameDay,\n    layoutAllDay,\n    layoutEvents,\n    type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\nimport { dateTimeFormat } from \"@/utils/intl-cache\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n    /** Events to place. Instants are read in the browser's local time. */\n    events: readonly SchedulerEvent[];\n    /** Any day within the range to show. Default: today. */\n    anchor?: Date;\n    /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n    days?: number;\n    /** First visible hour, `0`–`23`. Default `8`. */\n    startHour?: number;\n    /** Last visible hour, `1`–`24`. Default `20`. */\n    endHour?: number;\n    /** Minutes a click on empty space snaps to. Default `30`. */\n    snapMinutes?: number;\n    /** Called when an event is activated by click, `Enter` or `Space`. */\n    onEventClick?: (event: SchedulerEvent) => void;\n    /** Called with the snapped instant when empty space is clicked. */\n    onSlotClick?: (start: Date) => void;\n    /** Render an event's contents. Defaults to its title and start time. */\n    renderEvent?: (event: SchedulerEvent) => ReactNode;\n    /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n    locale?: string;\n    /** Draw the current-time line. Default `true`. */\n    showCurrentTime?: boolean;\n    /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n    now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n *     events={bookings}\n *     days={7}\n *     startHour={7}\n *     endHour={21}\n *     onEventClick={(e) => open(e.id)}\n *     onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n    events,\n    anchor,\n    days: dayCount = 7,\n    startHour = 8,\n    endHour = 20,\n    snapMinutes = 30,\n    onEventClick,\n    onSlotClick,\n    renderEvent,\n    locale = \"pt-BR\",\n    showCurrentTime = true,\n    now,\n    className,\n    ...rest\n}: SchedulerProps) {\n    const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n    /**\n     * Keep the current-time line moving.\n     *\n     * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n     * to be deterministic, and a timer would fight it.\n     */\n    useEffect(() => {\n        if (now || !showCurrentTime) return;\n        const id = setInterval(() => setClock(new Date()), TICK_MS);\n        return () => clearInterval(id);\n    }, [now, showCurrentTime]);\n\n    const reference = now ?? clock;\n\n    const window = useMemo<DayWindow>(\n        () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n        [startHour, endHour],\n    );\n\n    /**\n     * The calendar day the range starts from, as a stable string.\n     *\n     * `reference` ticks every minute when the current-time line is live. Depending on\n     * the Date itself would re-slice the day range — and therefore relayout every\n     * event — once a minute; depending on the day only recomputes at midnight.\n     */\n    const anchorDay = (anchor ?? reference).toDateString();\n    const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n    const placed = useMemo(\n        () => layoutEvents({ events, days: dayList, window }),\n        [events, dayList, window],\n    );\n    const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n    const marks = useMemo(() => hourMarks(window), [window]);\n\n    const dayLabel = dateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n    const timeLabel = dateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n    /** Turn a click's vertical position within a day column into a snapped instant. */\n    const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n        const rect = event.currentTarget.getBoundingClientRect();\n        const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n        const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n        const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n        const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n        const result = new Date(day);\n        result.setHours(0, clamped, 0, 0);\n        return result;\n    };\n\n    const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n    const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n    return (\n        <div className={cn(styles.wrapper, className)} {...rest}>\n            <div\n                className={styles.head}\n                style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n            >\n                <span className={styles.gutterHead} />\n                {dayList.map((day, index) => (\n                    <span\n                        key={day.toISOString()}\n                        className={cn(styles.dayHead, index === todayIndex && styles.today)}\n                    >\n                        {dayLabel.format(day)}\n                    </span>\n                ))}\n            </div>\n\n            {allDay.length > 0 && (\n                <div\n                    className={styles.allDayLane}\n                    style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n                >\n                    <span className={styles.gutterHead}>Dia inteiro</span>\n                    <div className={styles.allDayTrack}>\n                        {allDay.map(({ event, dayIndex, span }) => (\n                            <button\n                                key={event.id}\n                                type=\"button\"\n                                className={styles.allDayEvent}\n                                style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n                                onClick={() => onEventClick?.(event)}\n                                disabled={!onEventClick}\n                            >\n                                {event.title}\n                            </button>\n                        ))}\n                    </div>\n                </div>\n            )}\n\n            {/*\n             * Focusable because it scrolls vertically: a scroll region that cannot be\n             * focused is unreachable by keyboard, which is what `axe`'s\n             * `scrollable-region-focusable` rule is about. It needs a name too, or the\n             * new tab stop would announce nothing — but `group`, not `region`: a named\n             * `region` is a landmark, and two schedulers on one page would then be two\n             * identically-named landmarks (`landmark-unique`).\n             */}\n            <div\n                className={styles.body}\n                style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n                tabIndex={0}\n                role=\"group\"\n                aria-label=\"Grade de horários\"\n            >\n                <div className={styles.gutter}>\n                    {marks.map((minute) => (\n                        <span\n                            key={minute}\n                            className={styles.hourLabel}\n                            style={{\n                                top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n                            }}\n                        >\n                            {timeLabel.format(\n                                new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n                            )}\n                        </span>\n                    ))}\n                </div>\n\n                {/*\n                 * Not `role=\"grid\"`: that requires `row` children, and the events are\n                 * siblings of the day columns inside one CSS grid — a `row` wrapper\n                 * would stop the columns being grid items and collapse the layout.\n                 * A labelled group per day is the honest structure anyway: a screen\n                 * reader tabs the event buttons and the group name supplies the day.\n                 */}\n                <div className={styles.grid} aria-label=\"Agenda\">\n                    {dayList.map((day, index) => (\n                        <div\n                            key={day.toISOString()}\n                            className={cn(\n                                styles.dayColumn,\n                                index === todayIndex && styles.todayColumn,\n                            )}\n                            role=\"group\"\n                            aria-label={dayLabel.format(day)}\n                            onClick={\n                                onSlotClick\n                                    ? (event) => {\n                                          // Only empty space creates: a click that\n                                          // landed on an event is that event's.\n                                          if (event.target !== event.currentTarget) return;\n                                          onSlotClick(slotFromClick(day, event));\n                                      }\n                                    : undefined\n                            }\n                        >\n                            {marks.map((minute) => (\n                                <span\n                                    key={minute}\n                                    className={styles.hourLine}\n                                    style={{\n                                        top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n                                    }}\n                                />\n                            ))}\n                        </div>\n                    ))}\n\n                    {placed.map((item) => (\n                        <button\n                            key={`${item.event.id}-${item.dayIndex}`}\n                            type=\"button\"\n                            className={styles.event}\n                            style={{\n                                gridColumn: `${item.dayIndex + 1} / span 1`,\n                                top: `${item.top * 100}%`,\n                                height: `${item.height * 100}%`,\n                                left: `${(item.column / item.columns) * 100}%`,\n                                width: `${(1 / item.columns) * 100}%`,\n                            }}\n                            onClick={() => onEventClick?.(item.event)}\n                            disabled={!onEventClick}\n                            title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n                        >\n                            {renderEvent ? (\n                                renderEvent(item.event)\n                            ) : (\n                                <>\n                                    <span className={styles.eventTime}>\n                                        {timeLabel.format(item.event.start)}\n                                    </span>\n                                    <span className={styles.eventTitle}>{item.event.title}</span>\n                                </>\n                            )}\n                        </button>\n                    ))}\n\n                    {currentFraction !== null && todayIndex >= 0 && (\n                        <span\n                            className={styles.nowLine}\n                            style={{ top: `${currentFraction * 100}%` }}\n                            aria-hidden\n                            data-testid=\"scheduler-now\"\n                        />\n                    )}\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}