{
  "name": "task-calendar",
  "type": "registry:block",
  "title": "Task Calendar",
  "description": "An operational planning matrix with week columns, clickable day cells, task previews and day details.",
  "registryDependencies": [
    "@siteplane/base",
    "@siteplane/button",
    "@siteplane/dialog",
    "@siteplane/popover",
    "@siteplane/badge",
    "@siteplane/resource-list"
  ],
  "dependencies": [
    "lucide-react@1.18.0"
  ],
  "files": [
    {
      "path": "src/components/patterns/task-calendar.tsx",
      "type": "registry:block",
      "target": "components/patterns/task-calendar.tsx",
      "content": "\"use client\";\n\nimport {\n  CalendarDaysIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  MapPinIcon,\n  UserRoundIcon,\n  XIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogHeader,\n  DialogPanel,\n  DialogPopup,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Popover,\n  PopoverClose,\n  PopoverDescription,\n  PopoverPopup,\n  PopoverTitle,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  ResourceList,\n  ResourceListContent,\n  ResourceListIndicator,\n  ResourceListItem,\n  ResourceListMeta,\n  ResourceListTitle,\n} from \"@/components/ui/resource-list\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TaskCalendarTaskAccent = \"chart-1\" | \"chart-2\" | \"chart-3\" | \"chart-4\" | \"chart-5\" | \"muted\";\nexport type TaskCalendarDayDialogSize = \"default\" | \"wide\";\nexport type TaskCalendarDayDialogSidebarDensity = \"default\" | \"compact\";\n\nexport type TaskCalendarTask = {\n  accent?: TaskCalendarTaskAccent;\n  id: string;\n  date: Date | string;\n  title: string;\n  assignee?: string;\n  calendarVisible?: boolean;\n  detail?: string;\n  status?: string;\n};\n\ntype TaskCalendarTaskResolver = (\n  tasks: readonly TaskCalendarTask[],\n  day: Date,\n) => readonly TaskCalendarTask[];\ntype TaskCalendarDayTaskResolver = (day: Date) => readonly TaskCalendarTask[];\ntype TaskCalendarDayTransitionDirection = \"next\" | \"none\" | \"previous\";\n\nexport type TaskCalendarDayDetailRenderProps = {\n  day: Date;\n  selectedTask?: TaskCalendarTask;\n  tasks: readonly TaskCalendarTask[];\n};\n\ntype TaskCalendarDayDetailRenderer = (\n  props: TaskCalendarDayDetailRenderProps,\n) => React.ReactNode;\n\ntype TaskCalendarDayHeaderEndRenderer = (\n  props: TaskCalendarDayDetailRenderProps,\n) => React.ReactNode;\n\nexport type TaskCalendarProps = Omit<\n  React.ComponentPropsWithoutRef<\"section\">,\n  \"children\"\n> & {\n  activeDate?: Date | string;\n  activeTaskId?: string;\n  dayDialogSidebarDensity?: TaskCalendarDayDialogSidebarDensity;\n  dayDialogSize?: TaskCalendarDayDialogSize;\n  getDetailTasks?: TaskCalendarTaskResolver;\n  getVisibleTasks?: TaskCalendarTaskResolver;\n  maxVisibleTasks?: number;\n  onDaySelect?: (day: Date, tasks: readonly TaskCalendarTask[]) => void;\n  onTaskSelect?: (task: TaskCalendarTask) => void;\n  renderDayDetail?: TaskCalendarDayDetailRenderer;\n  renderDayHeaderEnd?: TaskCalendarDayHeaderEndRenderer;\n  startDate: Date | string;\n  tasks: readonly TaskCalendarTask[];\n  todayDate?: Date | string;\n  weeks?: number;\n};\n\nconst weekdayLabels = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] as const;\n\nconst dayDialogPopupClassNames = {\n  default: \"w-[min(74rem,calc(100vw-2rem))]\",\n  wide: \"w-[min(88rem,calc(100vw-1rem))]\",\n} satisfies Record<TaskCalendarDayDialogSize, string>;\n\nconst dayDialogGridClassNames = {\n  default: \"sm:grid-cols-[18rem_minmax(0,1fr)]\",\n  wide: \"sm:grid-cols-[14rem_minmax(0,1fr)]\",\n} satisfies Record<TaskCalendarDayDialogSize, string>;\n\nconst accentClassNames = {\n  \"chart-1\":\n    \"border-chart-1/24 bg-chart-1/8 hover:bg-chart-1/12 data-pressed:bg-chart-1/16\",\n  \"chart-2\":\n    \"border-chart-2/24 bg-chart-2/8 hover:bg-chart-2/12 data-pressed:bg-chart-2/16\",\n  \"chart-3\":\n    \"border-chart-3/24 bg-chart-3/8 hover:bg-chart-3/12 data-pressed:bg-chart-3/16\",\n  \"chart-4\":\n    \"border-chart-4/24 bg-chart-4/8 hover:bg-chart-4/12 data-pressed:bg-chart-4/16\",\n  \"chart-5\":\n    \"border-chart-5/24 bg-chart-5/8 hover:bg-chart-5/12 data-pressed:bg-chart-5/16\",\n  muted:\n    \"border-input bg-muted/64 text-muted-foreground hover:bg-muted data-pressed:bg-muted\",\n} satisfies Record<TaskCalendarTaskAccent, string>;\n\nconst overlaySecondaryTextClassName =\n  \"text-muted-foreground dark:text-foreground/64\";\n\nfunction getTaskAccent(task: TaskCalendarTask): TaskCalendarTaskAccent {\n  return task.accent ?? \"muted\";\n}\n\nconst defaultGetVisibleTasks: TaskCalendarTaskResolver = (tasks) =>\n  tasks.filter((task) => task.calendarVisible === true);\n\nconst defaultGetDetailTasks: TaskCalendarTaskResolver = (tasks) => {\n  return tasks;\n};\n\nexport function TaskCalendar({\n  activeDate,\n  activeTaskId,\n  className,\n  dayDialogSidebarDensity = \"default\",\n  dayDialogSize = \"default\",\n  getDetailTasks = defaultGetDetailTasks,\n  getVisibleTasks = defaultGetVisibleTasks,\n  maxVisibleTasks = 2,\n  onDaySelect,\n  onTaskSelect,\n  renderDayDetail,\n  renderDayHeaderEnd,\n  startDate,\n  tasks,\n  todayDate,\n  weeks = 7,\n  ...props\n}: TaskCalendarProps): React.ReactElement {\n  const weekCount = Math.max(1, weeks);\n  const visibleTaskLimit = Math.max(0, maxVisibleTasks);\n\n  const baseCalendarStart = React.useMemo(\n    () => startOfWeek(parseTaskCalendarDate(startDate)),\n    [startDate],\n  );\n  const [calendarWeekOffset, setCalendarWeekOffset] = React.useState(0);\n  const calendarStart = React.useMemo(\n    () => addDays(baseCalendarStart, calendarWeekOffset * 7),\n    [baseCalendarStart, calendarWeekOffset],\n  );\n  const calendarEnd = React.useMemo(\n    () => addDays(calendarStart, weekCount * 7 - 1),\n    [calendarStart, weekCount],\n  );\n  const visibleRangeLabel = formatCalendarRangeLabel(calendarStart, calendarEnd);\n  const handleCalendarWeekChange = React.useCallback((offset: number) => {\n    setCalendarWeekOffset((currentOffset) => currentOffset + offset);\n  }, []);\n  const activeDayKey = activeDate\n    ? formatDateKey(parseTaskCalendarDate(activeDate))\n    : undefined;\n  const todayKey = formatDateKey(parseTaskCalendarDate(todayDate ?? new Date()));\n\n  const columns = React.useMemo(\n    () =>\n      Array.from({ length: weekCount }, (_, weekIndex) => {\n        const weekStart = addDays(calendarStart, weekIndex * 7);\n\n        return {\n          days: weekdayLabels.map((_, dayIndex) => addDays(weekStart, dayIndex)),\n          end: addDays(weekStart, 6),\n          start: weekStart,\n        };\n      }),\n    [calendarStart, weekCount],\n  );\n\n  const tasksByDay = React.useMemo(() => {\n    const groupedTasks = new Map<string, TaskCalendarTask[]>();\n\n    for (const task of tasks) {\n      const key = formatDateKey(parseTaskCalendarDate(task.date));\n      const dayTasks = groupedTasks.get(key) ?? [];\n\n      dayTasks.push(task);\n      groupedTasks.set(key, dayTasks);\n    }\n\n    return groupedTasks;\n  }, [tasks]);\n  const getDialogDayTasks = React.useCallback<TaskCalendarDayTaskResolver>(\n    (targetDay) => {\n      const dayTasks = tasksByDay.get(formatDateKey(targetDay)) ?? [];\n\n      return getDetailTasks(dayTasks, targetDay);\n    },\n    [getDetailTasks, tasksByDay],\n  );\n\n  return (\n    <section\n      aria-label=\"Task calendar\"\n      className={cn(\"grid min-w-0 gap-3\", className)}\n      {...props}\n    >\n      <div className=\"rounded-lg border bg-background\">\n        <div\n          className=\"flex min-w-0 items-center justify-between gap-2 border-b px-2 py-1.5\"\n          data-slot=\"task-calendar-week-navigation\"\n        >\n          <Button\n            aria-label=\"Show previous week\"\n            className=\"size-8 shrink-0\"\n            data-slot=\"task-calendar-previous-week-trigger\"\n            onClick={() => handleCalendarWeekChange(-1)}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <ChevronLeftIcon />\n          </Button>\n          <div\n            aria-live=\"polite\"\n            className=\"min-w-0 text-center\"\n            data-slot=\"task-calendar-visible-range\"\n          >\n            <p className=\"truncate font-medium text-sm\">{visibleRangeLabel}</p>\n            <p className={cn(\"truncate text-xs\", overlaySecondaryTextClassName)}>\n              {formatVisibleWeekCount(weekCount)}\n            </p>\n          </div>\n          <Button\n            aria-label=\"Show next week\"\n            className=\"size-8 shrink-0\"\n            data-slot=\"task-calendar-next-week-trigger\"\n            onClick={() => handleCalendarWeekChange(1)}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <ChevronRightIcon />\n          </Button>\n        </div>\n\n        <div className=\"overflow-x-auto\">\n          <div\n            className=\"grid w-full min-w-max\"\n            style={{\n              gridTemplateColumns: `2.75rem repeat(${columns.length}, minmax(10rem, 10rem))`,\n            }}\n          >\n            <div className=\"border-r border-b bg-muted/40\" />\n            {columns.map((column) => (\n              <div\n                className=\"border-r border-b bg-muted/40 px-2 py-1.5 text-center font-medium text-muted-foreground text-xs last:border-r-0\"\n                key={formatDateKey(column.start)}\n              >\n                {formatWeekRange(column.start, column.end)}\n              </div>\n            ))}\n\n            {weekdayLabels.map((weekday, dayIndex) => (\n              <React.Fragment key={weekday}>\n                <div className=\"border-r border-b bg-muted/30 px-2 py-3 text-center font-medium text-muted-foreground text-xs\">\n                  {weekday}\n                </div>\n                {columns.map((column) => {\n                  const day = column.days[dayIndex];\n                  const dayKey = formatDateKey(day);\n                  const dayTasks = tasksByDay.get(dayKey) ?? [];\n                  const detailTasks = getDetailTasks(dayTasks, day);\n                  const visibleTasks = getVisibleTasks(dayTasks, day).slice(\n                    0,\n                    visibleTaskLimit,\n                  );\n                  const taskCount = detailTasks.length;\n                  const isActive = dayKey === activeDayKey;\n                  const isToday = dayKey === todayKey;\n\n                  return (\n                    <DayCell\n                      dayDialogSidebarDensity={dayDialogSidebarDensity}\n                      dayDialogSize={dayDialogSize}\n                      day={day}\n                      detailTasks={detailTasks}\n                      getDialogDayTasks={getDialogDayTasks}\n                      isActive={isActive}\n                      isToday={isToday}\n                      key={dayKey}\n                      onDaySelect={onDaySelect}\n                      onTaskSelect={onTaskSelect}\n                      renderDayDetail={renderDayDetail}\n                      renderDayHeaderEnd={renderDayHeaderEnd}\n                      taskCount={taskCount}\n                      activeTaskId={activeTaskId}\n                      visibleTasks={visibleTasks}\n                    />\n                  );\n                })}\n              </React.Fragment>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nfunction DayCell({\n  activeTaskId,\n  day,\n  dayDialogSidebarDensity,\n  dayDialogSize,\n  detailTasks,\n  getDialogDayTasks,\n  isActive,\n  isToday,\n  onDaySelect,\n  onTaskSelect,\n  renderDayDetail,\n  renderDayHeaderEnd,\n  taskCount,\n  visibleTasks,\n}: {\n  activeTaskId?: string;\n  day: Date;\n  dayDialogSidebarDensity: TaskCalendarDayDialogSidebarDensity;\n  dayDialogSize: TaskCalendarDayDialogSize;\n  detailTasks: readonly TaskCalendarTask[];\n  getDialogDayTasks: TaskCalendarDayTaskResolver;\n  isActive: boolean;\n  isToday: boolean;\n  onDaySelect?: (day: Date, tasks: readonly TaskCalendarTask[]) => void;\n  onTaskSelect?: (task: TaskCalendarTask) => void;\n  renderDayDetail?: TaskCalendarDayDetailRenderer;\n  renderDayHeaderEnd?: TaskCalendarDayHeaderEndRenderer;\n  taskCount: number;\n  visibleTasks: readonly TaskCalendarTask[];\n}): React.ReactElement {\n  const [isDayDialogOpen, setDayDialogOpen] = React.useState(false);\n  const [selectedDialogTaskId, setSelectedDialogTaskId] = React.useState<\n    string | undefined\n  >();\n  const onTaskDetailOpen = React.useCallback(\n    (task: TaskCalendarTask) => {\n      setSelectedDialogTaskId(task.id);\n      setDayDialogOpen(true);\n      onTaskSelect?.(task);\n    },\n    [onTaskSelect],\n  );\n  const onCountTaskSelect = onTaskDetailOpen;\n  const handleDayTriggerOpen = React.useCallback(() => {\n    setSelectedDialogTaskId(activeTaskId);\n    setDayDialogOpen(true);\n    onDaySelect?.(day, detailTasks);\n  }, [activeTaskId, day, detailTasks, onDaySelect]);\n  const dialogSelectionKey = [\n    formatDateKey(day),\n    selectedDialogTaskId ?? activeTaskId ?? \"\",\n    detailTasks.map((task) => task.id).join(\"|\"),\n  ].join(\":\");\n\n  return (\n    <div\n      className={cn(\n        \"relative min-h-24 border-r border-b bg-background last:border-r-0 data-active:bg-muted/40 dark:data-active:bg-input/24\",\n        isToday\n          ? \"ring-1 ring-inset ring-primary/22 shadow-[inset_0_0_14px_1px_color-mix(in_oklab,var(--primary)_24%,transparent),inset_0_0_36px_color-mix(in_oklab,var(--primary)_8%,transparent)]\"\n          : isActive && \"ring-1 ring-inset ring-border/64\",\n      )}\n      data-active={isActive ? \"\" : undefined}\n      data-today={isToday ? \"\" : undefined}\n      onClick={(event) => {\n        const target = event.target;\n\n        if (!(target instanceof Element)) {\n          return;\n        }\n\n        if (!event.currentTarget.contains(target)) {\n          return;\n        }\n\n        if (isInteractiveTaskCalendarTarget(target)) {\n          return;\n        }\n\n        handleDayTriggerOpen();\n      }}\n    >\n      <Dialog open={isDayDialogOpen} onOpenChange={setDayDialogOpen}>\n        <DialogTrigger\n          render={\n            <Button\n              aria-current={isToday ? \"date\" : undefined}\n              aria-label={formatDayDetailsLabel(day, detailTasks)}\n              className=\"absolute inset-0 z-0 h-full w-full rounded-none border border-transparent bg-transparent p-0 hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-0 data-pressed:bg-accent/50 sm:h-full\"\n              data-slot=\"task-calendar-day-trigger\"\n              onClick={handleDayTriggerOpen}\n              variant=\"ghost\"\n            />\n          }\n        >\n          <span className=\"sr-only\">{formatDayDetailsLabel(day, detailTasks)}</span>\n        </DialogTrigger>\n        <TaskDayDialog\n          activeTaskId={activeTaskId}\n          day={day}\n          dayDialogSidebarDensity={dayDialogSidebarDensity}\n          dayDialogSize={dayDialogSize}\n          initialTaskId={selectedDialogTaskId}\n          getDialogDayTasks={getDialogDayTasks}\n          key={dialogSelectionKey}\n          onDaySelect={onDaySelect}\n          renderDayDetail={renderDayDetail}\n          renderDayHeaderEnd={renderDayHeaderEnd}\n        />\n      </Dialog>\n\n      <div className=\"pointer-events-none relative z-10 grid min-h-24 content-start gap-1.5 p-2\">\n        <div className=\"flex min-w-0 items-center justify-between gap-2\">\n          <div\n            className={cn(\n              \"flex size-6 items-center justify-center rounded-full font-medium text-xs\",\n              isActive && \"bg-muted/72 text-foreground dark:bg-input/64\",\n              isToday && \"bg-primary text-primary-foreground\",\n              !isActive && !isToday && \"text-foreground\",\n            )}\n          >\n            {day.getDate()}\n          </div>\n\n          {taskCount > 0 ? (\n            <Popover>\n              <PopoverTrigger\n                render={\n                  <Button\n                    aria-label={formatTaskListLabel(day, detailTasks)}\n                    className=\"pointer-events-auto h-6 min-w-6 rounded-sm px-1.5 font-medium text-xs shadow-none before:shadow-none focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 not-disabled:not-active:not-data-pressed:before:shadow-none dark:not-disabled:not-active:not-data-pressed:before:shadow-none sm:h-6\"\n                    data-slot=\"task-calendar-task-count-trigger\"\n                    variant=\"outline\"\n                  />\n                }\n              >\n                {taskCount}\n              </PopoverTrigger>\n              <PopoverPopup align=\"end\" className=\"w-80\" side=\"right\">\n                <DayTaskList\n                  day={day}\n                  onTaskSelect={onCountTaskSelect}\n                  tasks={detailTasks}\n                />\n              </PopoverPopup>\n            </Popover>\n          ) : null}\n        </div>\n\n        <div className=\"grid min-w-0 gap-1\">\n          {visibleTasks.map((task) => (\n            <TaskPill\n              isActive={activeTaskId === task.id}\n              key={task.id}\n              onOpenTaskDetail={onTaskDetailOpen}\n              onTaskSelect={onTaskSelect}\n              task={task}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction isInteractiveTaskCalendarTarget(target: Element): boolean {\n  return Boolean(\n    target.closest(\"button, a, input, select, textarea, [role=button]\") ||\n      target.closest(\"[data-slot=task-calendar-day-dialog]\"),\n  );\n}\n\nfunction TaskPill({\n  isActive,\n  onOpenTaskDetail,\n  onTaskSelect,\n  task,\n}: {\n  isActive: boolean;\n  onOpenTaskDetail?: (task: TaskCalendarTask) => void;\n  onTaskSelect?: (task: TaskCalendarTask) => void;\n  task: TaskCalendarTask;\n}): React.ReactElement {\n  const accent = getTaskAccent(task);\n\n  return (\n    <Popover>\n      <PopoverTrigger\n        render={\n          <Button\n            aria-current={isActive ? \"true\" : undefined}\n            aria-label={formatTaskDetailsLabel(task)}\n            className={cn(\n              \"pointer-events-auto h-6 min-w-0 w-full justify-start overflow-hidden rounded-md border px-1.5 text-left text-xs data-active:border-border data-active:bg-muted/72 data-active:text-foreground dark:data-active:bg-input/64 sm:h-6\",\n              accentClassNames[accent],\n              isActive &&\n                \"border-border bg-muted/72 text-foreground dark:bg-input/64\",\n            )}\n            data-active={isActive ? \"\" : undefined}\n            data-slot=\"task-calendar-task-trigger\"\n            onClick={() => onTaskSelect?.(task)}\n            variant=\"ghost\"\n          />\n        }\n      >\n        <span className=\"flex min-w-0 items-center gap-2\">\n          {task.assignee ? (\n            <UserRoundIcon className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n          ) : (\n            <MapPinIcon className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n          )}\n          <span className=\"truncate\">{task.title}</span>\n        </span>\n      </PopoverTrigger>\n\n      <PopoverPopup align=\"start\" className=\"w-72\">\n        <TaskDetails\n          onOpenTaskDetail={() => onOpenTaskDetail?.(task)}\n          task={task}\n        />\n      </PopoverPopup>\n    </Popover>\n  );\n}\n\nfunction TaskDayDialog({\n  activeTaskId,\n  day,\n  dayDialogSidebarDensity,\n  dayDialogSize,\n  getDialogDayTasks,\n  initialTaskId,\n  onDaySelect,\n  renderDayDetail,\n  renderDayHeaderEnd,\n}: {\n  activeTaskId?: string;\n  day: Date;\n  dayDialogSidebarDensity: TaskCalendarDayDialogSidebarDensity;\n  dayDialogSize: TaskCalendarDayDialogSize;\n  getDialogDayTasks: TaskCalendarDayTaskResolver;\n  initialTaskId?: string;\n  onDaySelect?: (day: Date, tasks: readonly TaskCalendarTask[]) => void;\n  renderDayDetail?: TaskCalendarDayDetailRenderer;\n  renderDayHeaderEnd?: TaskCalendarDayHeaderEndRenderer;\n}): React.ReactElement {\n  const isCompactSidebar = dayDialogSidebarDensity === \"compact\";\n  const [currentDay, setCurrentDay] = React.useState(day);\n  const [dayTransitionDirection, setDayTransitionDirection] =\n    React.useState<TaskCalendarDayTransitionDirection>(\"none\");\n  const currentDayKey = formatDateKey(currentDay);\n  const currentTasks = getDialogDayTasks(currentDay);\n  const [previousDialogDay, setPreviousDialogDay] = React.useState<\n    Date | undefined\n  >();\n  const [previousDialogSelectedTaskId, setPreviousDialogSelectedTaskId] =\n    React.useState<string | undefined>();\n  const initialSelectedTaskId = React.useMemo(() => {\n    const initialTask = initialTaskId\n      ? currentTasks.find((task) => task.id === initialTaskId)\n      : undefined;\n    const activeTask = activeTaskId\n      ? currentTasks.find((task) => task.id === activeTaskId)\n      : undefined;\n\n    return initialTask?.id ?? activeTask?.id ?? currentTasks[0]?.id;\n  }, [activeTaskId, currentTasks, initialTaskId]);\n  const [selectedTaskId, setSelectedTaskId] = React.useState(initialSelectedTaskId);\n  const handleDialogDayChange = React.useCallback(\n    (offset: number) => {\n      const nextDay = addDays(currentDay, offset);\n      const nextTasks = getDialogDayTasks(nextDay);\n\n      setPreviousDialogDay(currentDay);\n      setPreviousDialogSelectedTaskId(selectedTaskId);\n      setDayTransitionDirection(offset > 0 ? \"next\" : \"previous\");\n      setCurrentDay(nextDay);\n      setSelectedTaskId(nextTasks[0]?.id);\n      onDaySelect?.(nextDay, nextTasks);\n    },\n    [currentDay, getDialogDayTasks, onDaySelect, selectedTaskId],\n  );\n  const onExitAnimationEnd = React.useCallback(() => {\n    setPreviousDialogDay(undefined);\n    setPreviousDialogSelectedTaskId(undefined);\n  }, []);\n\n  const previousDayKey = previousDialogDay\n    ? formatDateKey(previousDialogDay)\n    : undefined;\n  const previousTasks = previousDialogDay\n    ? getDialogDayTasks(previousDialogDay)\n    : [];\n  const previousSelectedTask =\n    previousTasks.find((task) => task.id === previousDialogSelectedTaskId) ??\n    previousTasks[0];\n  const selectedTask =\n    currentTasks.find((task) => task.id === selectedTaskId) ?? currentTasks[0];\n  const previousCustomDetail = previousDialogDay\n    ? renderDayDetail?.({\n        day: previousDialogDay,\n        selectedTask: previousSelectedTask,\n        tasks: previousTasks,\n      })\n    : undefined;\n\n  const previousDetail = previousDialogDay ? (\n    previousCustomDetail ?? (\n      <TaskDayDialogDefaultDetail\n        day={previousDialogDay}\n        selectedTask={previousSelectedTask}\n        tasks={previousTasks}\n      />\n    )\n  ) : undefined;\n  const previousTaskList = previousDialogDay ? (\n    <TaskDayDialogTaskList\n      activeTaskId={activeTaskId}\n      isCompactSidebar={isCompactSidebar}\n      onTaskSelect={setSelectedTaskId}\n      selectedTask={previousSelectedTask}\n      tasks={previousTasks}\n    />\n  ) : undefined;\n  const currentTaskList = (\n    <TaskDayDialogTaskList\n      activeTaskId={activeTaskId}\n      isCompactSidebar={isCompactSidebar}\n      onTaskSelect={setSelectedTaskId}\n      selectedTask={selectedTask}\n      tasks={currentTasks}\n    />\n  );\n\n  const customDetail = renderDayDetail?.({\n    day: currentDay,\n    selectedTask,\n    tasks: currentTasks,\n  });\n  const customHeaderEnd = renderDayHeaderEnd?.({\n    day: currentDay,\n    selectedTask,\n    tasks: currentTasks,\n  });\n\n  return (\n    <DialogPopup\n      bottomStickOnMobile={false}\n      className={cn(\n        \"max-h-[min(44rem,calc(100vh-2rem))] max-w-none overflow-hidden border-0 bg-transparent p-0 shadow-none before:hidden\",\n        dayDialogPopupClassNames[dayDialogSize],\n      )}\n      showCloseButton={false}\n    >\n      <div\n        className={cn(\n          \"grid min-h-0 flex-1 gap-3 overflow-hidden\",\n          dayDialogGridClassNames[dayDialogSize],\n        )}\n        data-slot=\"task-calendar-day-dialog\"\n      >\n        <aside\n          className=\"flex min-h-0 flex-col overflow-hidden rounded-xl border bg-popover not-dark:bg-clip-padding p-3 text-popover-foreground shadow-lg/5\"\n          data-slot=\"task-calendar-day-dialog-sidebar\"\n        >\n          <div\n            className=\"mb-3 grid min-w-0 shrink-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 px-1\"\n            data-slot=\"task-calendar-day-dialog-sidebar-header\"\n          >\n            <Button\n              aria-label=\"Show previous day\"\n              className=\"size-8 shrink-0\"\n              data-slot=\"task-calendar-day-dialog-previous-trigger\"\n              onClick={() => handleDialogDayChange(-1)}\n              size=\"icon\"\n              variant=\"ghost\"\n            >\n              <ChevronLeftIcon />\n            </Button>\n            <div\n              aria-live=\"polite\"\n              className=\"min-w-0 text-center\"\n              data-slot=\"task-calendar-day-dialog-day-summary\"\n            >\n              <p className=\"truncate font-semibold text-sm leading-tight\">\n                {formatCompactDayLabel(currentDay)}\n              </p>\n              <p\n                className={cn(\n                  \"mt-0.5 truncate text-xs leading-tight\",\n                  overlaySecondaryTextClassName,\n                )}\n              >\n                {formatTaskCount(currentTasks.length)}\n              </p>\n            </div>\n            <Button\n              aria-label=\"Show next day\"\n              className=\"size-8 shrink-0\"\n              data-slot=\"task-calendar-day-dialog-next-trigger\"\n              onClick={() => handleDialogDayChange(1)}\n              size=\"icon\"\n              variant=\"ghost\"\n            >\n              <ChevronRightIcon />\n            </Button>\n          </div>\n\n          <TaskCalendarDayTransitionPanel\n            activeKey={currentDayKey}\n            direction={dayTransitionDirection}\n            exiting={previousTaskList}\n            exitingKey={previousDayKey}\n            className=\"min-h-0 flex-1\"\n            contentClassName=\"min-h-0 overflow-hidden\"\n            onExitAnimationEnd={onExitAnimationEnd}\n            slot=\"task-calendar-day-dialog-day-panel\"\n          >\n            {currentTaskList}\n          </TaskCalendarDayTransitionPanel>\n        </aside>\n\n        <section\n          className=\"relative flex min-h-0 min-w-0 flex-col overflow-hidden rounded-xl border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5\"\n          data-slot=\"task-calendar-day-dialog-main\"\n        >\n          <div data-slot=\"task-calendar-day-dialog-main-header\">\n            <DialogHeader className=\"border-b px-5 py-4 pr-14\">\n              <div className=\"flex min-w-0 items-start justify-between gap-4\">\n                <div className=\"flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1\">\n                  <DialogTitle className=\"min-w-0 line-clamp-2 text-lg\">\n                    {selectedTask?.title ?? \"No task selected\"}\n                  </DialogTitle>\n                  {selectedTask?.assignee ? (\n                    <span\n                      className={cn(\n                        \"shrink-0 text-sm\",\n                        overlaySecondaryTextClassName,\n                      )}\n                    >\n                      {selectedTask.assignee}\n                    </span>\n                  ) : null}\n                </div>\n                {customHeaderEnd ? (\n                  <div\n                    className=\"ml-auto min-w-0 shrink-0\"\n                    data-slot=\"task-calendar-day-dialog-main-header-end\"\n                  >\n                    {customHeaderEnd}\n                  </div>\n                ) : null}\n              </div>\n            </DialogHeader>\n            <DialogClose\n              aria-label=\"Close dialog\"\n              className=\"absolute end-2 top-2\"\n              render={<Button size=\"icon\" variant=\"ghost\" />}\n            >\n              <XIcon />\n            </DialogClose>\n          </div>\n\n          <DialogPanel className=\"min-h-80 p-4 sm:p-5\">\n            <TaskCalendarDayTransitionPanel\n              activeKey={currentDayKey}\n              direction={dayTransitionDirection}\n              exiting={previousDetail}\n              exitingKey={previousDayKey}\n              onExitAnimationEnd={onExitAnimationEnd}\n              slot=\"task-calendar-day-dialog-detail\"\n            >\n              {customDetail ?? (\n                <TaskDayDialogDefaultDetail\n                  day={currentDay}\n                  selectedTask={selectedTask}\n                  tasks={currentTasks}\n                />\n              )}\n            </TaskCalendarDayTransitionPanel>\n          </DialogPanel>\n        </section>\n      </div>\n    </DialogPopup>\n  );\n}\n\nfunction TaskCalendarDayTransitionPanel({\n  activeKey,\n  children,\n  className,\n  contentClassName,\n  direction,\n  exiting,\n  exitingKey,\n  onExitAnimationEnd,\n  slot,\n}: {\n  activeKey: string;\n  children: React.ReactNode;\n  className?: string;\n  contentClassName?: string;\n  direction: TaskCalendarDayTransitionDirection;\n  exiting?: React.ReactNode;\n  exitingKey?: string;\n  onExitAnimationEnd?: () => void;\n  slot: string;\n}): React.ReactElement {\n  const shouldAnimate = direction !== \"none\";\n\n  return (\n    <div className={cn(\"grid min-h-0 overflow-hidden\", className)}>\n      {shouldAnimate && exitingKey ? (\n        <div\n          aria-hidden=\"true\"\n          className={cn(\n            \"siteplane-task-calendar-day-motion pointer-events-none col-start-1 row-start-1 min-w-0\",\n            contentClassName,\n          )}\n          data-direction={direction}\n          data-motion-state=\"exit\"\n          key={`exit:${exitingKey}`}\n          onAnimationEnd={(event) => {\n            if (event.currentTarget === event.target) {\n              onExitAnimationEnd?.();\n            }\n          }}\n        >\n          {exiting}\n        </div>\n      ) : null}\n      <div\n        className={cn(\n          \"siteplane-task-calendar-day-motion col-start-1 row-start-1 min-w-0\",\n          contentClassName,\n        )}\n        data-direction={direction}\n        data-motion-state=\"enter\"\n        data-slot={slot}\n        key={`enter:${activeKey}`}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction TaskDayDialogTaskList({\n  activeTaskId,\n  isCompactSidebar,\n  onTaskSelect,\n  selectedTask,\n  tasks,\n}: {\n  activeTaskId?: string;\n  isCompactSidebar: boolean;\n  onTaskSelect: (taskId: string) => void;\n  selectedTask?: TaskCalendarTask;\n  tasks: readonly TaskCalendarTask[];\n}): React.ReactElement {\n  return (\n    <ResourceList\n      className=\"max-h-full min-h-0 content-start overflow-y-auto pr-1\"\n      density=\"compact\"\n      variant=\"soft\"\n    >\n      {tasks.length > 0 ? (\n        tasks.map((task) => {\n          const isSelected = task.id === selectedTask?.id;\n\n          return (\n            <ResourceListItem\n              active={activeTaskId === task.id}\n              aria-current={isSelected ? \"true\" : undefined}\n              interactive\n              key={task.id}\n              className={cn(isCompactSidebar && \"gap-2 px-2 py-1.5\")}\n              onClick={() => onTaskSelect(task.id)}\n              render={<button type=\"button\" />}\n              selected={isSelected}\n            >\n              <ResourceListIndicator tone={getTaskAccent(task)} />\n              <ResourceListContent>\n                <ResourceListTitle className={cn(isCompactSidebar && \"text-xs\")}>\n                  {task.title}\n                </ResourceListTitle>\n              </ResourceListContent>\n              <ResourceListMeta\n                className={cn(\n                  overlaySecondaryTextClassName,\n                  isCompactSidebar && \"text-xs\",\n                )}\n              >\n                {task.assignee ?? task.status ?? \"Details\"}\n              </ResourceListMeta>\n            </ResourceListItem>\n          );\n        })\n      ) : (\n        <p\n          className={cn(\n            \"rounded-md border border-dashed p-3 text-sm\",\n            overlaySecondaryTextClassName,\n          )}\n        >\n          No tasks on this day.\n        </p>\n      )}\n    </ResourceList>\n  );\n}\n\nfunction TaskDayDialogDefaultDetail({\n  selectedTask,\n}: TaskCalendarDayDetailRenderProps): React.ReactElement {\n  if (!selectedTask) {\n    return (\n      <div className=\"grid min-h-72 place-items-center rounded-md border border-dashed bg-muted/30 p-6 text-center\">\n        <div className=\"grid gap-1\">\n          <p className=\"font-medium text-sm\">No task selected</p>\n          <p className={cn(\"text-sm\", overlaySecondaryTextClassName)}>\n            Select a task from the list to view its details.\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"grid gap-4\">\n      {selectedTask.detail ? (\n        <p\n          className={cn(\n            \"rounded-md bg-muted/40 p-3 text-sm leading-relaxed\",\n            overlaySecondaryTextClassName,\n          )}\n        >\n          {selectedTask.detail}\n        </p>\n      ) : (\n        <p\n          className={cn(\n            \"rounded-md bg-muted/40 p-3 text-sm\",\n            overlaySecondaryTextClassName,\n          )}\n        >\n          No description has been added for this task.\n        </p>\n      )}\n\n      <div className=\"grid gap-2 rounded-md border border-dashed bg-background p-4\">\n        <p className=\"font-medium text-sm\">Detail area</p>\n        <p className={cn(\"text-sm\", overlaySecondaryTextClassName)}>\n          No detail content has been added yet.\n        </p>\n      </div>\n    </div>\n  );\n}\n\nfunction DayTaskList({\n  day,\n  onTaskSelect,\n  tasks,\n}: {\n  day: Date;\n  onTaskSelect?: (task: TaskCalendarTask) => void;\n  tasks: readonly TaskCalendarTask[];\n}): React.ReactElement {\n  return (\n    <div className=\"grid gap-3\">\n      <div className=\"grid gap-1\">\n        <PopoverTitle className=\"text-base\">{formatDayLabel(day)}</PopoverTitle>\n        <PopoverDescription className={overlaySecondaryTextClassName}>\n          {tasks.length > 0\n            ? `${formatTaskCount(tasks.length)} in the detail list.`\n            : \"No tasks in the detail list.\"}\n        </PopoverDescription>\n      </div>\n\n      <ResourceList density=\"compact\" variant=\"soft\">\n        {tasks.length > 0 ? (\n          tasks.map((task) => (\n            <ResourceListItem\n              interactive\n              key={task.id}\n              onClick={() => onTaskSelect?.(task)}\n              render={<PopoverClose render={<button type=\"button\" />} />}\n            >\n              <ResourceListIndicator tone={getTaskAccent(task)} />\n              <ResourceListContent>\n                <ResourceListTitle>{task.title}</ResourceListTitle>\n              </ResourceListContent>\n              <ResourceListMeta className={overlaySecondaryTextClassName}>\n                {task.assignee ?? task.status ?? \"Details\"}\n              </ResourceListMeta>\n            </ResourceListItem>\n          ))\n        ) : (\n          <p\n            className={cn(\n              \"rounded-md border border-dashed p-3 text-sm\",\n              overlaySecondaryTextClassName,\n            )}\n          >\n            No day details available.\n          </p>\n        )}\n      </ResourceList>\n    </div>\n  );\n}\n\nfunction TaskDetails({\n  onOpenTaskDetail,\n  task,\n}: {\n  onOpenTaskDetail?: () => void;\n  task: TaskCalendarTask;\n}): React.ReactElement {\n  return (\n    <div className=\"grid gap-3\">\n      <div className=\"grid gap-1\">\n        <PopoverTitle className=\"text-base\">{task.title}</PopoverTitle>\n        <PopoverDescription className={overlaySecondaryTextClassName}>\n          Task details and next steps.\n        </PopoverDescription>\n      </div>\n      <div className=\"grid gap-2 text-sm\">\n        <div className=\"flex items-center gap-2\">\n          <CalendarDaysIcon\n            className={cn(\"size-4\", overlaySecondaryTextClassName)}\n            aria-hidden=\"true\"\n          />\n          <span>{formatDayLabel(parseTaskCalendarDate(task.date))}</span>\n        </div>\n        {task.assignee ? (\n          <div className=\"flex items-center gap-2\">\n            <UserRoundIcon\n              className={cn(\"size-4\", overlaySecondaryTextClassName)}\n              aria-hidden=\"true\"\n            />\n            <span>{task.assignee}</span>\n          </div>\n        ) : null}\n        {task.detail ? (\n          <p\n            className={cn(\n              \"rounded-md bg-muted/40 p-2 text-sm\",\n              overlaySecondaryTextClassName,\n            )}\n          >\n            {task.detail}\n          </p>\n        ) : null}\n      </div>\n      {onOpenTaskDetail ? (\n        <PopoverClose\n          onClick={onOpenTaskDetail}\n          render={<Button className=\"w-full\" size=\"sm\" variant=\"secondary\" />}\n        >\n          Open in day details\n        </PopoverClose>\n      ) : null}\n    </div>\n  );\n}\n\nfunction formatDayDetailsLabel(\n  day: Date,\n  tasks: readonly TaskCalendarTask[],\n): string {\n  return `Open calendar details for ${formatDayLabel(day)}, ${formatTaskCount(tasks.length)}`;\n}\n\nfunction formatTaskListLabel(day: Date, tasks: readonly TaskCalendarTask[]): string {\n  return `Show task list for ${formatDayLabel(day)}, ${formatTaskCount(tasks.length)}`;\n}\n\nfunction formatTaskDetailsLabel(task: TaskCalendarTask): string {\n  return `Open details for task ${task.title}`;\n}\n\nfunction formatTaskCount(count: number): string {\n  return count === 1 ? \"1 task\" : `${count} tasks`;\n}\n\nfunction parseTaskCalendarDate(value: Date | string): Date {\n  if (value instanceof Date) {\n    return new Date(value.getFullYear(), value.getMonth(), value.getDate());\n  }\n\n  const isoDate = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n\n  if (isoDate) {\n    return new Date(Number(isoDate[1]), Number(isoDate[2]) - 1, Number(isoDate[3]));\n  }\n\n  const parsedDate = new Date(value);\n\n  return new Date(\n    parsedDate.getFullYear(),\n    parsedDate.getMonth(),\n    parsedDate.getDate(),\n  );\n}\n\nfunction startOfWeek(date: Date): Date {\n  const weekday = date.getDay();\n  const offset = weekday === 0 ? -6 : 1 - weekday;\n\n  return addDays(date, offset);\n}\n\nfunction addDays(date: Date, days: number): Date {\n  const nextDate = new Date(date);\n\n  nextDate.setDate(nextDate.getDate() + days);\n\n  return nextDate;\n}\n\nfunction formatWeekRange(start: Date, end: Date): string {\n  return `${formatShortDate(start)} - ${formatShortDate(end)}`;\n}\n\nfunction formatCalendarRangeLabel(start: Date, end: Date): string {\n  return `${formatCompactDayLabel(start)} - ${formatCompactDayLabel(end)}`;\n}\n\nfunction formatVisibleWeekCount(count: number): string {\n  return count === 1 ? \"1 week\" : `${count} weeks`;\n}\n\nfunction formatDayLabel(date: Date): string {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    day: \"2-digit\",\n    month: \"2-digit\",\n    weekday: \"long\",\n    year: \"numeric\",\n  }).format(date);\n}\n\nfunction formatCompactDayLabel(date: Date): string {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    day: \"2-digit\",\n    month: \"long\",\n    year: \"numeric\",\n  }).format(date);\n}\n\nfunction formatShortDate(date: Date): string {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    day: \"numeric\",\n    month: \"short\",\n  }).format(date);\n}\n\nfunction formatDateKey(date: Date): string {\n  return [\n    date.getFullYear(),\n    padDatePart(date.getMonth() + 1),\n    padDatePart(date.getDate()),\n  ].join(\"-\");\n}\n\nfunction padDatePart(value: number): string {\n  return String(value).padStart(2, \"0\");\n}\n"
    }
  ],
  "categories": [
    "patterns"
  ],
  "docs": "# Task Calendar\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nTask Calendar is a `registry:block` for operational planning matrices: weeks are columns, weekdays are rows, day cells are clickable, and day details open in a large dialog.\n\nOnly tasks with `calendarVisible` appear directly in a cell by default. Every task can still remain available through the day count and detail list.\n\n## Import\n\n```tsx\nimport {\n  TaskCalendar,\n  type TaskCalendarTask,\n} from \"@/components/patterns/task-calendar\";\n```\n\n## Public API\n\n- `startDate` is normalized to Monday. `weeks` defaults to seven and controls the visible planning range.\n- `calendarVisible` controls direct cell visibility; `getVisibleTasks` can replace that rule.\n- `getDetailTasks` controls the day detail list. The default keeps every task for that day.\n- `maxVisibleTasks` limits task pills per cell; set it to `0` to keep all tasks in the detail list only.\n- `accent` accepts `chart-1` through `chart-5` or `muted`. Accent is supplementary; keep status or assignee available as text.\n- `dayDialogSize=\"wide\"` expands the detail dialog. `dayDialogSidebarDensity=\"compact\"` tightens only its task selector.\n- `renderDayDetail` replaces the right detail region; `renderDayHeaderEnd` extends its header. Consumer fetching and product logic remain outside the pattern.\n- `activeDate`, `activeTaskId`, `onDaySelect` and `onTaskSelect` connect the pattern to controlled product state or routing.\n\n## Public Motion\n\n- Week navigation is immediate. Day navigation inside the open dialog uses the shared Calendar slide tokens.\n- Dialog, Popover and Resource List behavior remains native to the underlying primitives. Reduced motion removes non-essential movement.\n\n## Public Accessibility\n\n- Keep day cells, task pills and task-count controls as separate focusable targets with clear accessible names.\n- Day-navigation buttons in the dialog need explicit previous/next labels and must not compete with task selection.\n- Keep task identity, status and assignee available as text; accent color is supplementary.\n- Preserve the native Dialog focus trap, Escape dismissal and labelled title relationship.\n\n## Public Agent Guidance\n\n- Use Task Calendar for operational planning; do not turn the date-picker Calendar primitive into a planning matrix.\n- Put task-type filtering in data or resolver functions rather than hardcoding product categories in the pattern.\n- Use `renderDayDetail` for product-specific detail content and keep task identity in the provided header.\n- Keep a direct path from every visible task pill to the full day-detail dialog.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/patterns/task-calendar)\n\n- [Registry source](https://ui.siteplane.io/r/task-calendar.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/patterns/task-calendar",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
