shadimage
openstatus
blockstatus-barsource: ok · refreshed

Status Bar

Interactive status timeline with hover, keyboard navigation, and event display

npx shadcn add "https://openstatus.dev/r/status-bar.json"
free

Where it goes

statskanban

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
date-fns

Registry dependencies

https://openstatus.dev/r/status-types.jsonhttps://openstatus.dev/r/status-utils.jsonhttps://openstatus.dev/r/status-i18n.jsonhover-cardseparatorskeletonhttps://openstatus.dev/r/use-media-query.json

Files1 · source cached 10/8/2026, 5:32:54 AM

src/components/blocks/status-bar.tsx
"use client";

import { useStatusBlocksLabels } from "@/components/blocks/status-i18n";
import type {
  StatusBarData,
  StatusEventType,
  StatusReportImpact,
  StatusType,
} from "@/components/blocks/status.types";
import { statusColors } from "@/components/blocks/status.utils";
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
import { formatDistanceStrict } from "date-fns";
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";

interface StatusBarProps {
  data: StatusBarData[];
  renderCard?: (
    data: StatusBarData["card"][number],
    index: number,
  ) => React.ReactNode;
  renderBar?: (
    data: StatusBarData["bar"][number],
    index: number,
  ) => React.ReactNode;
  renderEvent?: (
    data: StatusBarData["events"][number],
    index: number,
  ) => React.ReactNode;
  /**
   * Optional Radix Portal container for the day hover-card. Defaults to
   * `document.body`. Pass a ref when the bar is rendered inside a scoped
   * subtree (e.g. a status-page preview that overrides `--radius` and other
   * CSS vars on a wrapper) so the portaled card inherits the same context.
   */
  container?: HTMLElement | null;
}

interface UseStatusBarProps {
  dataLength: number;
  isTouch: boolean;
}

type InteractionType = "pin" | "hover" | "focus" | null;

/**
 * useStatusBar - Headless hook for managing status bar interactions and keyboard navigation
 *
 * This hook provides the core logic for StatusBar interactions, implementing a
 * headless UI pattern that separates state management from presentation. It handles:
 *
 * **Interaction Modes**:
 * - **hover**: Desktop hover state (shows card on mouse hover, auto-hides on leave)
 * - **pin**: Pinned state (click to pin card open, click again or Esc to close)
 * - **focus**: Keyboard focus state (shows card while focused, hides on blur)
 *
 * **Keyboard Navigation**:
 * - **Arrow Left/Right**: Navigate between bars in the same timeline
 * - **Arrow Up/Down**: Navigate between timelines (different monitors)
 * - **Enter/Space**: Pin/unpin the active bar
 * - **Escape**: Close pinned card and remove focus
 *
 * **Touch Device Support**:
 * - Disables hover state on touch devices (detected via `(hover: none)` media query)
 * - Click interaction works on both touch and non-touch devices
 *
 * **Outside Click Handling**:
 * - Automatically closes pinned cards when clicking outside the component
 *
 * @param dataLength - Total number of bars in the timeline
 * @param isTouch - Whether the device supports touch (no hover capability)
 *
 * @returns Hook state and handlers:
 * - `activeIndex`: Currently active bar index (null if none)
 * - `isOpen`: Whether a card is currently displayed
 * - `interactionType`: Current interaction mode ("pin" | "hover" | "focus" | null)
 * - `containerRef`: Ref for the status bar container (for outside click detection)
 * - `handlers`: Event handler functions for mouse/keyboard/focus interactions
 * - `setButtonRef`: Function to register bar element refs (for keyboard navigation)
 *
 * @example
 * ```tsx
 * function CustomStatusBar({ data }) {
 *   const isTouch = useMediaQuery("(hover: none)");
 *   const { activeIndex, handlers, setButtonRef, containerRef } = useStatusBar({
 *     dataLength: data.length,
 *     isTouch,
 *   });
 *
 *   return (
 *     <div ref={containerRef} role="toolbar">
 *       {data.map((item, index) => (
 *         <button
 *           key={index}
 *           ref={(el) => setButtonRef(index, el)}
 *           onClick={() => handlers.onClick(index)}
 *           onFocus={() => handlers.onFocus(index)}
 *           onKeyDown={(e) => handlers.onKeyDown(e, index)}
 *         >
 *           {// Custom bar rendering}
 *         </button>
 *       ))}
 *     </div>
 *   );
 * }
 * ```
 *
 * @see StatusBar - For the complete implementation using this hook
 */
function useStatusBar({ dataLength, isTouch }: UseStatusBarProps) {
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
  const [interactionType, setInteractionType] = useState<InteractionType>(null);
  const buttonRefs = useRef<(HTMLElement | null)[]>([]);
  const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Clear hover timeout on unmount
  useEffect(() => {
    return () => {
      if (hoverTimeoutRef.current) {
        clearTimeout(hoverTimeoutRef.current);
      }
    };
  }, []);

  // Handle clicks outside to close pinned card
  useEffect(() => {
    if (interactionType !== "pin" || activeIndex === null) return;

    const handleOutsideClick = (e: MouseEvent) => {
      if (
        containerRef.current &&
        !containerRef.current.contains(e.target as Node)
      ) {
        setActiveIndex(null);
        setInteractionType(null);
      }
    };

    document.addEventListener("mousedown", handleOutsideClick);
    return () => document.removeEventListener("mousedown", handleOutsideClick);
  }, [interactionType, activeIndex]);

  const clearHoverTimeout = useCallback(() => {
    if (hoverTimeoutRef.current) {
      clearTimeout(hoverTimeoutRef.current);
      hoverTimeoutRef.current = null;
    }
  }, []);

  const interactionTypeRef = useRef<InteractionType>(null);
  useEffect(() => {
    interactionTypeRef.current = interactionType;
  }, [interactionType]);

  const handleClick = useCallback(
    (index: number) => {
      clearHoverTimeout();
      setActiveIndex((prev) => {
        if (prev === index && interactionTypeRef.current === "pin") {
          setInteractionType(null);
          return null;
        }
        setInteractionType("pin");
        return index;
      });
    },
    [clearHoverTimeout],
  );

  const handleHoverStart = useCallback(
    (index: number) => {
      // On touch devices, don't show hover state
      if (isTouch) return;

      clearHoverTimeout();
      setActiveIndex(index);
      setInteractionType("hover");
    },
    [isTouch, clearHoverTimeout],
  );

  const handleHoverEnd = useCallback(() => {
    // Only clear hover state, not pinned or focused
    if (interactionType !== "hover") return;

    hoverTimeoutRef.current = setTimeout(() => {
      setActiveIndex(null);
      setInteractionType(null);
    }, 100);
  }, [interactionType]);

  const handleFocus = useCallback((index: number) => {
    setActiveIndex(index);
    setInteractionType("focus");
  }, []);

  const handleBlur = useCallback((e: React.FocusEvent) => {
    // Only clear if not moving to another bar
    const relatedTarget = e.relatedTarget as HTMLElement;
    const isMovingToAnotherBar =
      relatedTarget &&
      relatedTarget.closest('[role="toolbar"]') === containerRef.current &&
      relatedTarget.getAttribute("role") === "button";

    if (!isMovingToAnotherBar) {
      setActiveIndex(null);
      setInteractionType(null);
    }
  }, []);

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent, currentIndex: number) => {
      switch (e.key) {
        case "Escape":
          e.preventDefault();
          setActiveIndex(null);
          setInteractionType(null);
          clearHoverTimeout();
          buttonRefs.current[currentIndex]?.blur();
          break;

        case "ArrowLeft":
          e.preventDefault();
          {
            const newIndex =
              currentIndex > 0 ? currentIndex - 1 : dataLength - 1;
            buttonRefs.current[newIndex]?.focus();
          }
          break;

        case "ArrowRight":
          e.preventDefault();
          {
            const newIndex =
              currentIndex < dataLength - 1 ? currentIndex + 1 : 0;
            buttonRefs.current[newIndex]?.focus();
          }
          break;

        case "ArrowUp":
          e.preventDefault();
          {
            // Navigate to previous monitor's status bar
            const prevMonitor = containerRef.current?.closest(
              '[data-slot="status-component"]',
            )?.previousElementSibling;
            if (prevMonitor) {
              const prevBar = prevMonitor.querySelector('[role="toolbar"]');
              if (prevBar) {
                const prevButtons = prevBar.querySelectorAll('[role="button"]');
                const targetButton = prevButtons[currentIndex] as HTMLElement;
                targetButton?.focus();
              }
            }
          }
          break;

        case "ArrowDown":
          e.preventDefault();
          {
            // Navigate to next monitor's status bar
            const nextMonitor = containerRef.current?.closest(
              '[data-slot="status-component"]',
            )?.nextElementSibling;
            if (nextMonitor) {
              const nextBar = nextMonitor.querySelector('[role="toolbar"]');
              if (nextBar) {
                const nextButtons = nextBar.querySelectorAll('[role="button"]');
                const targetButton = nextButtons[currentIndex] as HTMLElement;
                targetButton?.focus();
              }
            }
          }
          break;

        case "Enter":
        case " ":
          e.preventDefault();
          handleClick(currentIndex);
          break;
      }
    },
    [dataLength, clearHoverTimeout, handleClick],
  );

  const setButtonRef = useCallback((index: number, el: HTMLElement | null) => {
    buttonRefs.current[index] = el;
  }, []);

  return {
    activeIndex,
    isOpen: activeIndex !== null,
    interactionType,
    containerRef,
    handlers: {
      onClick: handleClick,
      onHoverStart: handleHoverStart,
      onHoverEnd: handleHoverEnd,
      onHoverCardEnter: clearHoverTimeout,
      onHoverCardLeave: () => {
        setActiveIndex(null);
        setInteractionType(null);
      },
      onFocus: handleFocus,
      onBlur: handleBlur,
      onKeyDown: handleKeyDown,
    },
    setButtonRef,
  };
}

/**
 * StatusBar - Interactive uptime timeline with keyboard navigation and hover cards
 *
 * Displays a horizontal timeline of status bars, where each bar represents a day's
 * status with color-coded segments. Interactive hover cards show detailed status
 * information, events, and incidents for each day.
 *
 * **Key Features**:
 * - **Visual Timeline**: Vertical bars with color-coded segments showing status over time
 * - **Interactive Cards**: Hover/click to see detailed breakdowns and events
 * - **Keyboard Navigation**: Full keyboard support with arrow keys, Enter, and Escape
 * - **Touch Support**: Optimized interactions for touch devices
 * - **Customizable Rendering**: Override default renderers for bars, cards, and events
 * - **Accessibility**: ARIA roles, labels, and keyboard navigation
 *
 * **Interaction Modes**:
 * - Desktop: Hover to preview, click to pin, Esc to close
 * - Touch: Tap to toggle card open/closed
 * - Keyboard: Arrow keys to navigate, Enter/Space to pin, Esc to close
 *
 * **Keyboard Navigation**:
 * - **Left/Right Arrows**: Move between bars in the timeline
 * - **Up/Down Arrows**: Navigate between different monitor timelines
 * - **Enter/Space**: Pin or unpin the active card
 * - **Escape**: Close card and remove focus
 *
 * **Data Structure**:
 * Each data item represents a day and contains:
 * - `day`: Date string
 * - `bar`: Array of segments with status and height percentage
 * - `card`: Array of status breakdowns to display in hover card
 * - `events`: Array of incidents/maintenance events for that day
 *
 * @param data - Array of status bar data items (one per day)
 * @param renderCard - Optional custom renderer for card content items
 * @param renderBar - Optional custom renderer for bar segments
 * @param renderEvent - Optional custom renderer for event badges
 *
 * @example
 * // Basic usage with default rendering
 * ```tsx
 * const uptimeData = [
 *   {
 *     day: "2024-01-15",
 *     bar: [{ status: "success", height: 100 }],
 *     card: [{ status: "success", value: "100%" }],
 *     events: []
 *   },
 *   {
 *     day: "2024-01-16",
 *     bar: [
 *       { status: "success", height: 80 },
 *       { status: "error", height: 20 }
 *     ],
 *     card: [
 *       { status: "success", value: "80%" },
 *       { status: "error", value: "20%" }
 *     ],
 *     events: [
 *       {
 *         id: "inc-1",
 *         type: "incident",
 *         name: "API Downtime",
 *         from: new Date("2024-01-16T10:00:00Z"),
 *         to: new Date("2024-01-16T10:30:00Z")
 *       }
 *     ]
 *   }
 * ];
 *
 * <StatusBar data={uptimeData} />
 * ```
 *
 * @example
 * // With custom bar renderer
 * ```tsx
 * <StatusBar
 *   data={uptimeData}
 *   renderBar={(segment, index) => (
 *     <div
 *       key={index}
 *       className="w-full transition-all"
 *       style={{
 *         height: `${segment.height}%`,
 *         background: `linear-gradient(to top, ${statusColors[segment.status]}, transparent)`
 *       }}
 *     />
 *   )}
 * />
 * ```
 *
 * @example
 * // With custom event renderer
 * ```tsx
 * <StatusBar
 *   data={uptimeData}
 *   renderEvent={(event, index) => (
 *     <Link key={index} href={`/incidents/${event.id}`}>
 *       <div className="text-sm hover:underline">
 *         {event.name}
 *       </div>
 *     </Link>
 *   )}
 * />
 * ```
 *
 * @see useStatusBar - For the headless hook powering the interactions
 * @see StatusBarSkeleton - For loading state
 * @see StatusBarEvent - For event badge rendering
 */
export function StatusBar({
  data,
  renderCard,
  renderBar,
  renderEvent,
  container,
}: StatusBarProps) {
  const labels = useStatusBlocksLabels();
  const isTouch = useMediaQuery("(hover: none)");
  const { activeIndex, interactionType, containerRef, handlers, setButtonRef } =
    useStatusBar({
      dataLength: data.length,
      isTouch,
    });

  return (
    <div
      ref={containerRef}
      className="flex h-[50px] w-full items-end gap-px"
      data-slot="status-bar"
      role="toolbar"
      aria-label={labels.ariaStatusTracker}
    >
      {data.map((item, index) => {
        const isActive = activeIndex === index;
        const isPinned = isActive && interactionType === "pin";

        return (
          <StatusBarItem
            key={item.day}
            ref={(el) => setButtonRef(index, el)}
            index={index}
            item={item}
            isActive={isActive}
            isPinned={isPinned}
            isTouch={isTouch}
            isLastItem={index === data.length - 1}
            handlers={handlers}
            renderCard={renderCard}
            renderBar={renderBar}
            renderEvent={renderEvent}
            container={container}
          />
        );
      })}
    </div>
  );
}
StatusBar.displayName = "StatusBar";

interface StatusBarItemProps {
  index: number;
  item: StatusBarData;
  isActive: boolean;
  isPinned: boolean;
  isTouch: boolean;
  isLastItem: boolean;
  handlers: ReturnType<typeof useStatusBar>["handlers"];
  renderCard?: StatusBarProps["renderCard"];
  renderBar?: StatusBarProps["renderBar"];
  renderEvent?: StatusBarProps["renderEvent"];
  container?: StatusBarProps["container"];
}

const StatusBarItem = forwardRef<HTMLDivElement, StatusBarItemProps>(
  (
    {
      index,
      item,
      isActive,
      isPinned,
      isTouch,
      isLastItem,
      handlers,
      renderCard,
      renderBar,
      renderEvent,
      container,
    },
    ref,
  ) => {
    const labels = useStatusBlocksLabels();
    return (
      <HoverCard openDelay={0} closeDelay={0} open={isActive}>
        <HoverCardTrigger asChild>
          <div
            ref={ref}
            className="group focus-visible:ring-ring/50 relative flex h-full flex-1 cursor-pointer flex-col rounded-lg outline-none hover:opacity-80 focus-visible:opacity-80 focus-visible:ring-[2px] aria-pressed:opacity-80"
            onClick={() => handlers.onClick(index)}
            onFocus={() => handlers.onFocus(index)}
            onBlur={handlers.onBlur}
            onMouseEnter={() => handlers.onHoverStart(index)}
            onMouseLeave={handlers.onHoverEnd}
            onKeyDown={(e) => handlers.onKeyDown(e, index)}
            tabIndex={isLastItem && !isActive ? 0 : isActive ? 0 : -1}
            role="button"
            aria-label={labels.ariaDayStatus(index + 1)}
            aria-pressed={isPinned}
            aria-expanded={isActive}
            data-slot="status-bar-item"
          >
            <div className="flex h-full w-full flex-col overflow-hidden rounded-lg">
              {/* Render bar segments */}
              {item.bar.map((segment, segmentIndex) => {
                if (renderBar) {
                  return renderBar(segment, segmentIndex);
                }
                return (
                  <div
                    key={`${item.day}-${segment.status}-${segmentIndex}`}
                    className="w-full transition-all"
                    style={{
                      height: `${segment.height}%`,
                      backgroundColor: statusColors[segment.status],
                    }}
                  />
                );
              })}
            </div>
          </div>
        </HoverCardTrigger>
        <HoverCardContent
          side="top"
          align="center"
          className="w-auto min-w-40 p-0"
          container={container}
          onMouseEnter={handlers.onHoverCardEnter}
          onMouseLeave={handlers.onHoverCardLeave}
          onPointerDownOutside={(e) => {
            // Prevent closing on touch devices when clicking the card
            if (isTouch) {
              e.preventDefault();
            }
          }}
        >
          <StatusBarCard
            item={item}
            isPinned={isPinned}
            isTouch={isTouch}
            renderCard={renderCard}
            renderEvent={renderEvent}
          />
        </HoverCardContent>
      </HoverCard>
    );
  },
);
StatusBarItem.displayName = "StatusBarItem";

export interface StatusBarCardProps {
  item: StatusBarData;
  isPinned?: boolean;
  isTouch?: boolean;
  renderCard?: StatusBarProps["renderCard"];
  renderEvent?: StatusBarProps["renderEvent"];
}

/**
 * StatusBarCard - Internal hover card content component
 *
 * Displays detailed status information for a single day in a hover card, including:
 * - Date header
 * - Status breakdown percentages
 * - Events/incidents for that day
 * - Pin/unpin instructions (when pinned on desktop)
 *
 * The card automatically formats the date and renders status items and events
 * using either custom renderers or default implementations.
 *
 * @param item - The status bar data for this day
 * @param isPinned - Whether the card is currently pinned open
 * @param isTouch - Whether the device supports touch
 * @param renderCard - Optional custom renderer for status items
 * @param renderEvent - Optional custom renderer for events
 */
export function StatusBarCard({
  item,
  isPinned = false,
  isTouch = false,
  renderCard,
  renderEvent,
}: StatusBarCardProps) {
  const labels = useStatusBlocksLabels();
  return (
    <div data-slot="status-bar-card">
      <div className="p-2 text-xs">
        {labels.formatDateShort(new Date(item.day))}
      </div>
      {item.card.length > 0 && (
        <>
          <Separator />
          <div className="space-y-1 p-2 text-sm">
            {item.card.map((cardItem, cardIndex) => {
              if (renderCard) {
                return renderCard(cardItem, cardIndex);
              }
              return (
                <StatusBarContent
                  key={`${item.day}-card-${cardIndex}`}
                  status={cardItem.status}
                  value={cardItem.value}
                  impact={cardItem.impact}
                />
              );
            })}
          </div>
        </>
      )}
      {item.events.length > 0 && (
        <>
          <Separator />
          <div className="p-2">
            {item.events.map((event, eventIndex) => {
              if (renderEvent) {
                return renderEvent(event, eventIndex);
              }
              return (
                <StatusBarEvent
                  key={`${event.id}-${event.type}`}
                  type={event.type}
                  name={event.name}
                  from={event.from}
                  to={event.to}
                  isAggregated={event.isAggregated}
                  status={event.status}
                />
              );
            })}
          </div>
        </>
      )}
      {isPinned && !isTouch && (
        <>
          <Separator />
          <div className="text-muted-foreground flex cursor-pointer items-center p-2 text-xs">
            <span>{labels.clickAgainToUnpin}</span>
            <kbd className="border-input bg-background text-muted-foreground ml-auto inline-flex h-5 max-h-5 min-w-5 items-center justify-center rounded-sm border px-1.5 font-mono text-[10px] font-medium">
              Esc
            </kbd>
          </div>
        </>
      )}
    </div>
  );
}
StatusBarCard.displayName = "StatusBarCard";

/**
 * StatusBarSkeleton - Loading skeleton for StatusBar
 *
 * Displays a skeleton loader matching the height and width of a StatusBar,
 * used while status data is being fetched.
 *
 * @example
 * ```tsx
 * {isLoading ? (
 *   <StatusBarSkeleton />
 * ) : (
 *   <StatusBar data={uptimeData} />
 * )}
 * ```
 *
 * @see StatusBar - For the actual status bar component
 */
export function StatusBarSkeleton({
  className,
  ...props
}: React.ComponentProps<typeof Skeleton>) {
  return (
    <Skeleton
      className={cn("bg-muted h-[50px] w-full rounded-none", className)}
      {...props}
    />
  );
}
StatusBarSkeleton.displayName = "StatusBarSkeleton";

/**
 * StatusBarContent - Internal component for status breakdown rows
 *
 * Displays a single status item in the hover card with a colored indicator,
 * status label, and percentage value. Used by StatusBarCard to show the
 * breakdown of statuses for a day.
 *
 * @param status - The status type (success, degraded, error, info, empty)
 * @param value - The percentage or count value to display
 *
 * @example
 * ```tsx
 * <StatusBarContent status="success" value="95.2%" />
 * <StatusBarContent status="error" value="4.8%" />
 * ```
 */
function StatusBarContent({
  status,
  value,
  impact,
}: {
  status: StatusType;
  value: string;
  impact?: StatusReportImpact;
}) {
  const labels = useStatusBlocksLabels();
  return (
    <div className="flex items-baseline gap-4" data-slot="status-bar-content">
      <div className="flex items-center gap-2">
        <div
          className="h-2.5 w-2.5 rounded-lg"
          style={{
            backgroundColor: statusColors[status],
          }}
        />
        <div className="text-sm">
          {impact
            ? labels.componentImpact[impact]
            : labels.requestStatus[status]}
        </div>
      </div>
      <div className="text-muted-foreground ml-auto font-mono text-xs tracking-tight">
        {value}
      </div>
    </div>
  );
}
StatusBarContent.displayName = "StatusBarContent";

/**
 * StatusBarEvent - Event badge for incidents and maintenance
 *
 * Displays an event within a status bar hover card, showing:
 * - Color-coded indicator (red for incidents, yellow for reports, blue for maintenance)
 * - Event name with truncation for long names
 * - Date range (formatted as "Since", "Until", or "Jan 15 - Jan 16")
 * - Duration (formatted as "2 hours", "ongoing", or "across 3 days" for multiple incidents)
 *
 * The component automatically determines the status color based on the event type
 * (incident → error, report → degraded, maintenance → info) unless an explicit
 * `status` is passed, e.g. the day's worst report impact.
 *
 * Returns null if no start date is provided.
 *
 * @param name - Event name or title
 * @param from - Event start date
 * @param to - Event end date (null for ongoing events)
 * @param type - Event type ("incident", "report", or "maintenance")
 *
 * @example
 * ```tsx
 * <StatusBarEvent
 *   type="incident"
 *   name="API Downtime"
 *   from={new Date("2024-01-15T10:00:00Z")}
 *   to={new Date("2024-01-15T10:30:00Z")}
 * />
 * // Displays: [●] API Downtime
 * //           Jan 15, 10:00 AM - 10:30 AM  30 minutes
 * ```
 *
 * @example
 * ```tsx
 * // Ongoing incident
 * <StatusBarEvent
 *   type="incident"
 *   name="Database connectivity issues"
 *   from={new Date()}
 *   to={null}
 * />
 * // Displays: [●] Database connectivity issues
 * //           Since Jan 15, 2:30 PM  ongoing
 * ```
 *
 * @see StatusBarCard - For the card that contains event badges
 * @see formatDateRange - For date range formatting
 */
export function StatusBarEvent({
  name,
  from,
  to,
  type,
  isAggregated,
  status: statusProp,
}: {
  name: string;
  from?: Date | null;
  to?: Date | null;
  type: StatusEventType;
  isAggregated?: boolean;
  /** Overrides the type-derived dot color (e.g. the day's worst report impact). */
  status?: Exclude<StatusType, "empty">;
}) {
  const labels = useStatusBlocksLabels();
  if (!from) return null;

  const status =
    statusProp ??
    (type === "incident" ? "error" : type === "report" ? "degraded" : "info");

  return (
    <div className="group relative text-sm" data-slot="status-bar-event">
      {/* NOTE: this is to make the text truncate based on the width of the sibling element */}
      {/* REMINDER: height needs to be equal the text height */}
      <div className="h-4 w-full" />
      <div className="text-muted-foreground hover:text-foreground absolute inset-0">
        <div className="flex items-center gap-2">
          <div
            className="h-2.5 w-2.5 shrink-0 rounded-lg"
            style={{
              backgroundColor: statusColors[status],
            }}
          />
          <div className="truncate">{name}</div>
        </div>
      </div>
      <div className="text-muted-foreground mt-1 text-xs">
        {labels.formatDateRange(from, to ?? undefined)}{" "}
        <span className="text-muted-foreground/70 ml-1.5 font-mono">
          {formatDuration({ from, to, name, type, isAggregated, labels })}
        </span>
      </div>
    </div>
  );
}
StatusBarEvent.displayName = "StatusBarEvent";

/**
 * formatDuration - Internal helper for formatting event durations
 *
 * Formats the duration of an event based on start/end dates:
 * - No start date: returns null
 * - No end date: returns "ongoing"
 * - Aggregated incidents (isAggregated): returns "across {duration}"
 * - Zero seconds duration: returns null (hides duration)
 * - Otherwise: returns formatted duration (e.g., "2 hours", "3 days")
 *
 * @returns Formatted duration string or null
 */
const formatDuration = ({
  from,
  to,
  isAggregated,
  labels,
}: React.ComponentProps<typeof StatusBarEvent> & {
  labels: ReturnType<typeof useStatusBlocksLabels>;
}) => {
  if (!from) return null;
  if (!to) return labels.ongoing;
  const duration = formatDistanceStrict(from, to);
  if (isAggregated) return labels.durationAcross(duration);
  if (duration === "0 seconds") return null;
  return duration;
};

More from openstatus

Browse all components in this library →