bigbullui logobigbullui
Componentdata

Gantt Lite

Interactive festival and stage timetable schedule with animated now indicator.

Preview

Live Preview
VENUE TIMETABLE & STUB SCHEDULE
CURRENT: 20:00
STAGE / ZONE
18:0019:0020:0021:0022:0023:0024:00
NOW
MAIN STAGE
2 ACTS
ARENA B
2 ACTS
ACOUSTIC DOME
1 ACTS

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add gantt-lite

Or install the full npm package:

npm install bigbullui

Usage

import { GanttLite } from "@/components/ui/gantt-lite";

<GanttLite startHour={18} endHour={24} liveHour={20} />

Source code

gantt-lite.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface GanttItem {
  id: string;
  title: string;
  stage: string;
  startHour: number; // 0 to 24 (e.g. 18.5 = 18:30)
  durationHours: number;
  tier?: "VIP" | "HEADLINER" | "SUPPORT" | "DJ";
}

export interface GanttLiteProps extends React.HTMLAttributes<HTMLDivElement> {
  stages?: string[];
  items?: GanttItem[];
  startHour?: number;
  endHour?: number;
  liveHour?: number;
  onSelectItem?: (item: GanttItem) => void;
  className?: string;
}

const DEFAULT_STAGES = ["MAIN STAGE", "ARENA B", "ACOUSTIC DOME"];

const DEFAULT_ITEMS: GanttItem[] = [
  { id: "g1", title: "THE NEON PULSE", stage: "MAIN STAGE", startHour: 18, durationHours: 2, tier: "HEADLINER" },
  { id: "g2", title: "CYBER STRINGS", stage: "MAIN STAGE", startHour: 20.5, durationHours: 2.5, tier: "HEADLINER" },
  { id: "g3", title: "SOLO ACOUSTIC", stage: "ARENA B", startHour: 19, durationHours: 1.5, tier: "SUPPORT" },
  { id: "g4", title: "MIDNIGHT ECHOES", stage: "ARENA B", startHour: 21, durationHours: 2, tier: "DJ" },
  { id: "g5", title: "SECRET STUB SET", stage: "ACOUSTIC DOME", startHour: 19.5, durationHours: 2, tier: "VIP" },
];

const TIER_COLORS: Record<string, string> = {
  HEADLINER: "border-accent bg-accent/20 text-accent font-bold",
  VIP: "border-foreground bg-foreground text-background font-bold",
  SUPPORT: "border-border bg-card text-foreground",
  DJ: "border-dashed border-border bg-secondary text-foreground",
};

export function GanttLite({
  stages = DEFAULT_STAGES,
  items = DEFAULT_ITEMS,
  startHour = 18,
  endHour = 24,
  liveHour = 20,
  onSelectItem,
  className,
  ...props
}: GanttLiteProps) {
  const totalHours = endHour - startHour;
  const hours = Array.from({ length: totalHours + 1 }, (_, i) => startHour + i);

  // Live line position percent
  const livePercent = Math.max(0, Math.min(100, ((liveHour - startHour) / totalHours) * 100));

  return (
    <div
      className={cn(
        "relative w-full overflow-x-auto rounded-lg border-2 border-foreground bg-card p-4 font-mono select-none shadow-sm",
        className
      )}
      {...props}
    >
      {/* Header bar */}
      <div className="flex items-center justify-between border-b-2 border-dashed border-border pb-3 mb-4">
        <div className="flex items-center gap-2">
          <span className="size-2 rounded-full bg-accent animate-ping" />
          <span className="text-xs font-bold uppercase tracking-wider text-foreground">
            VENUE TIMETABLE & STUB SCHEDULE
          </span>
        </div>
        <span className="rounded border border-accent bg-accent/10 px-2 py-0.5 text-[10px] font-bold text-accent">
          CURRENT: {liveHour.toString().padStart(2, "0")}:00
        </span>
      </div>

      <div className="min-w-[640px]">
        {/* Time column headers */}
        <div className="flex border-b border-border pb-2 text-[10px] font-bold text-muted-foreground">
          <div className="w-36 shrink-0 uppercase tracking-wider">STAGE / ZONE</div>
          <div className="relative flex flex-1 justify-between px-2">
            {hours.map((h) => (
              <span key={h} className="text-center w-10">
                {h}:00
              </span>
            ))}
          </div>
        </div>

        {/* Rows per stage */}
        <div className="relative divide-y divide-border/60">
          {/* Animated live line indicator */}
          <div
            style={{ left: `calc(9rem + (100% - 9rem) * ${livePercent / 100})` }}
            className="absolute top-0 bottom-0 z-20 pointer-events-none w-0.5 bg-accent"
          >
            <div className="absolute -top-2 -translate-x-1/2 rounded bg-accent px-1 text-[8px] font-bold text-accent-foreground">
              NOW
            </div>
          </div>

          {stages.map((stage) => {
            const stageItems = items.filter((item) => item.stage === stage);

            return (
              <div key={stage} className="relative flex items-center py-3 min-h-[56px]">
                {/* Stage label */}
                <div className="w-36 shrink-0 pr-3">
                  <div className="text-xs font-bold uppercase text-foreground truncate">
                    {stage}
                  </div>
                  <div className="text-[9px] text-muted-foreground uppercase">
                    {stageItems.length} ACTS
                  </div>
                </div>

                {/* Timeline track */}
                <div className="relative flex-1 h-10 rounded bg-secondary/40 border border-dashed border-border/50">
                  {stageItems.map((item) => {
                    const leftPercent = ((item.startHour - startHour) / totalHours) * 100;
                    const widthPercent = (item.durationHours / totalHours) * 100;

                    return (
                      <button
                        key={item.id}
                        type="button"
                        onClick={() => onSelectItem?.(item)}
                        style={{
                          left: `${leftPercent}%`,
                          width: `${widthPercent}%`,
                        }}
                        className={cn(
                          "absolute top-1 bottom-1 flex items-center justify-between rounded px-2 text-left cursor-pointer border shadow-xs transition-all hover:scale-[1.02] hover:z-30 hover:shadow-md",
                          TIER_COLORS[item.tier || "SUPPORT"]
                        )}
                      >
                        <span className="truncate text-[10px] uppercase font-bold tracking-tight">
                          {item.title}
                        </span>
                        {item.tier && (
                          <span className="ml-1 shrink-0 rounded bg-background/80 px-1 py-0.2 text-[8px] text-foreground">
                            {item.tier}
                          </span>
                        )}
                      </button>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Props

PropTypeDescription
stagesstring[]Stage / zone names list for rows.
itemsGanttItem[]Scheduled timetable acts and performance blocks.
startHournumberTimeline start hour (0-24, default 18).
endHournumberTimeline end hour (0-24, default 24).
liveHournumberCurrent hour position for the pulsing red now indicator line.
onSelectItem(item: GanttItem) => voidCallback when a schedule block is clicked.