abui

Timeline

Component 106

About

A Gantt-chart style scheduling timeline with rows and draggable time slots that snap to a configurable interval, with drop validation, drag ghost previews, a mouse time indicator, and a current-time marker.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Timeline to your project and fits it to your theme.

Code

How Timeline is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { useState } from "react";
import { Slider } from "@/components/ui/slider";
import {
  TimelineProvider,
  Timeline,
  TimelineGrid,
  TimelineRow,
  TimelineRowData,
  TimelineSlot,
  TimelineSlotData,
  TimelineSlotLabel,
  TimelineSlotContent,
  TimelineHeader,
  TimelineCurrentTime,
} from "@/components/ui/timeline";
import { Users, Clock } from "lucide-react";
import { cn } from "@/lib/utils";

const rows: TimelineRowData[] = [
  { id: "room-a", label: "Conference Room A", capacity: 12 },
  { id: "room-b", label: "Conference Room B", capacity: 8 },
  { id: "room-1", label: "Meeting Room 1", capacity: 6 },
  { id: "room-2", label: "Meeting Room 2", capacity: 4 },
  { id: "training", label: "Training Room", capacity: 20 },
];

const categoryConfig: Record<
  string,
  { dot: string; border: string; text: string }
> = {
  meeting: {
    dot: "bg-blue-500",
    border: "border-l-blue-500",
    text: "text-blue-600 dark:text-blue-400",
  },
  workshop: {
    dot: "bg-emerald-500",
    border: "border-l-emerald-500",
    text: "text-emerald-600 dark:text-emerald-400",
  },
  break: {
    dot: "bg-purple-500",
    border: "border-l-purple-500",
    text: "text-purple-600 dark:text-purple-400",
  },
  review: {
    dot: "bg-orange-500",
    border: "border-l-orange-500",
    text: "text-orange-600 dark:text-orange-400",
  },
};

const initialSlots: (TimelineSlotData & {
  title: string;
  attendees: number;
  category: string;
})[] = [
  {
    id: "1",
    rowId: "room-a",
    startTime: "09:30",
    duration: 30,
    title: "Team Standup",
    attendees: 8,
    category: "meeting",
  },
  {
    id: "2",
    rowId: "room-a",
    startTime: "11:00",
    duration: 60,
    title: "Product Review",
    attendees: 6,
    category: "review",
  },
  {
    id: "3",
    rowId: "room-b",
    startTime: "10:00",
    duration: 90,
    title: "Design Workshop",
    attendees: 5,
    category: "workshop",
  },
  {
    id: "4",
    rowId: "room-b",
    startTime: "14:00",
    duration: 30,
    title: "Client Meeting",
    attendees: 4,
    category: "meeting",
  },
  {
    id: "5",
    rowId: "room-1",
    startTime: "09:00",
    duration: 30,
    title: "Quick Sync",
    attendees: 3,
    category: "meeting",
  },
  {
    id: "6",
    rowId: "room-1",
    startTime: "13:00",
    duration: 30,
    title: "Coffee Break",
    attendees: 10,
    category: "break",
  },
  {
    id: "7",
    rowId: "room-2",
    startTime: "10:30",
    duration: 60,
    title: "Code Review",
    attendees: 4,
    category: "review",
  },
  {
    id: "8",
    rowId: "room-2",
    startTime: "15:00",
    duration: 30,
    title: "1-on-1",
    attendees: 2,
    category: "meeting",
  },
  {
    id: "9",
    rowId: "training",
    startTime: "09:00",
    duration: 120,
    title: "React Workshop",
    attendees: 15,
    category: "workshop",
  },
  {
    id: "10",
    rowId: "training",
    startTime: "14:30",
    duration: 90,
    title: "Security Training",
    attendees: 18,
    category: "workshop",
  },
];

export default function Page() {
  const [slots, setSlots] = useState(initialSlots);
  const [percentageInView, setPercentageInView] = useState(100);

  const config = {
    startHour: 9,
    endHour: 18,
    snapIntervalMinutes: 15,
    columnWidth: 160,
  };

  const handlePositionChange = async (
    slotId: string,
    newTime: string,
    newRowId: string,
  ) => {
    setSlots((prev) =>
      prev.map((s) =>
        s.id === slotId ? { ...s, startTime: newTime, rowId: newRowId } : s,
      ),
    );
    await new Promise((resolve) => setTimeout(resolve, 100));
    return true;
  };

  return (
    <div className="w-full">
      <div className="flex flex-col gap-2 mb-4 w-full">
        <p className="text-sm font-medium leading-none">Zoom level</p>
        <Slider
          min={50}
          max={100}
          step={1}
          value={[percentageInView]}
          onValueChange={(value) => setPercentageInView(value[0])}
        />
      </div>

      <div className="flex flex-wrap gap-4 text-sm mb-4">
        {[
          ["Meeting", "meeting"],
          ["Workshop", "workshop"],
          ["Break", "break"],
          ["Review", "review"],
        ].map(([label, key]) => (
          <div key={key} className="flex items-center gap-1.5">
            <span
              className={cn(
                "h-2.5 w-2.5 rounded-full",
                categoryConfig[key].dot,
              )}
            />
            {label}
          </div>
        ))}
      </div>

      <TimelineProvider
        config={config}
        percentageInView={percentageInView}
        onSlotPositionChange={handlePositionChange}
      >
        <Timeline slots={slots} rows={rows}>
          <TimelineGrid>
            <TimelineHeader columnLabel="Rooms" />
            {rows.map((row) => (
              <TimelineRow
                key={row.id}
                row={row}
                slots={slots}
                renderRowHeader={(r) => (
                  <div className="px-4 py-1">
                    <div className="font-medium text-sm">{r.label}</div>
                    <div className="flex items-center gap-1 text-xs text-muted-foreground">
                      <Users className="h-3 w-3" /> {r.capacity}
                    </div>
                  </div>
                )}
              >
                {(slot) => {
                  const s = slot as (typeof initialSlots)[number];
                  const cat = categoryConfig[s.category];
                  return (
                    <TimelineSlot
                      key={s.id}
                      slot={s}
                      className={cn("border-l-4 bg-card", cat.border)}
                    >
                      <div className="px-2 py-1">
                        <TimelineSlotLabel className={cat.text}>
                          {s.title}
                        </TimelineSlotLabel>
                        <TimelineSlotContent className="flex items-center gap-2 text-muted-foreground">
                          <span className="flex items-center gap-0.5">
                            <Clock className="h-3 w-3" />
                            {s.startTime}
                          </span>
                          <span className="flex items-center gap-0.5">
                            <Users className="h-3 w-3" />
                            {s.attendees}
                          </span>
                        </TimelineSlotContent>
                      </div>
                    </TimelineSlot>
                  );
                }}
              </TimelineRow>
            ))}
            <TimelineCurrentTime />
          </TimelineGrid>
        </Timeline>
      </TimelineProvider>
    </div>
  );
}

Dependencies

More by abui

View all
  • Scroll Reveal Content A

    Scroll Reveal Content A

    318
  • Animated Chart

    Animated Chart

    5