npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pangeasi/lane-scheduler-react

v1.2.1

Published

Flexible drag-and-drop scheduler component for React

Downloads

335

Readme

Lane Scheduler

A flexible, drag-and-drop scheduler component for React with full TypeScript support.

Unit Agnostic: This library doesn't assume any specific unit of measurement. Slots represent abstract spaces that can be mapped to any unit - time intervals, physical spaces, resource allocations, or any other sequential organization system.

Interactive Documentation

Explore the complete Storybook documentation with interactive examples, API reference, and usage guides!

Features

  • 🎯 Drag & Drop - Move appointments between lanes with smooth animations
  • 📏 Resizable - Adjust appointment duration from both ends
  • 🔒 Locked Slots - Block specific slots with custom logic
  • 🎨 Customizable - Full control over rendering with render props
  • 📱 Mobile Ready - Touch events support
  • TypeScript - Full type safety
  • 🪶 Lightweight - No dependencies except React

Installation

npm install @pangeasi/lane-scheduler-react

Styles

The library comes with pre-built CSS that includes all necessary Tailwind styles. You need to import it in your application:

import "@pangeasi/lane-scheduler-react/styles.css";

The CSS file is self-contained and includes only the Tailwind utilities used by the components (~15KB gzipped).

Basic Usage

import { Scheduler, Lane } from "@pangeasi/lane-scheduler-react";
import "@pangeasi/lane-scheduler-react/styles.css";

function App() {
  const [appointments, setAppointments] = useState([
    { id: "1", startSlot: 2, duration: 3, title: "Meeting" },
  ]);

  const handleMove = (apt, sourceLane, targetLane, newSlot) => {
    // Handle appointment move
  };

  return (
    <Scheduler onAppointmentMove={handleMove}>
      <Lane
        laneId="room-1"
        appointments={appointments}
        totalSlots={24} // Could represent hours, rooms, days, etc.
        config={{
          height: 80,
          slotWidth: 60,
        }}
      />
    </Scheduler>
  );
}

Props

Scheduler

| Prop | Type | Description | | ------------------- | ----------- | ------------------------------- | | children | ReactNode | Lane components | | onAppointmentMove | function | Callback when appointment moves | | collisionStrategy | "reject" \| "swap" | Defaults to "reject". Use "swap" to exchange appointments when a dragged appointment is dropped over a non-overlappable appointment |

onAppointmentMove receives a fifth details argument when you need to handle swaps:

type AppointmentMoveDetails = {
  operation: "move" | "swap";
  appointment: Appointment;
  sourceLaneId: string;
  targetLaneId: string;
  newStartSlot: number;
  swappedAppointment?: Appointment;
  swappedAppointmentNewLaneId?: string;
  swappedAppointmentNewStartSlot?: number;
};

Lane

| Prop | Type | Default | Description | | --------------------------------- | --------------- | -------- | ------------------------------------------------- | | laneId | string | required | Unique identifier | | appointments | Appointment[] | [] | Array of appointments | | blockedSlots | number[] | [] | Blocked slot indices | | totalSlots | number | 24 | Total number of slots (unit agnostic) | | config | LaneConfig | {} | Visual configuration | | renderSlot | function | - | Custom slot renderer | | renderAppointmentContent | function | - | Custom appointment content | | onSlotClick | function | - | Slot click handler | | onSlotDoubleClick | function | - | Slot double-click handler | | onAppointmentChange | function | - | Appointment change handler | | onContextMenu | function | - | Slot right-click/long-press handler (default menu suppressed) | | appointmentContainerClassName | string | - | Custom className for appointment container | | appointmentResizerStartClassName | string | - | Custom className for start-edge resizer outer | | appointmentResizerEndClassName | string | - | Custom className for end-edge resizer outer | | appointmentResizerStartInnerClassName | string | - | Custom className for start-edge resizer inner line | | appointmentResizerEndInnerClassName | string | - | Custom className for end-edge resizer inner line |

Appointment Properties

| Property | Type | Description | | -------------- | --------- | -------------------------------------------------------------------------------------------------------- | | id | string | Unique identifier | | startSlot | number | Starting slot index | | duration | number | Number of slots the appointment spans | | title | string | Display text | | locked | boolean | If true, prevents dragging and resizing | | allowOverlap | boolean | If true, OTHER appointments are allowed to overlap with this one. Does NOT mean this can overlap others | | onBlockedSlot| function| Custom logic to handle blocked slots (return true to allow placement) |

Advanced Usage

Custom Slot Rendering

// Example: Time-based slots (30-minute intervals)
<Lane
  renderSlot={(slotIdx, isBlocked) => (
    <div>{slotIdx % 2 === 0 ? `${slotIdx / 2}:00` : ":30"}</div>
  )}
/>

// Example: Room-based slots
<Lane
  renderSlot={(slotIdx, isBlocked) => (
    <div>Room {slotIdx + 1}</div>
  )}
/>

// Example: Day-based slots
<Lane
  renderSlot={(slotIdx, isBlocked) => (
    <div>Day {slotIdx + 1}</div>
  )}
/>

Note: The library doesn't impose any unit interpretation. You define what each slot represents through your custom rendering logic.

Slot Interactions

Use the slot event handlers to trigger creation flows, detail drawers, or custom context menus. The native context menu is suppressed when onContextMenu is provided.

<Lane
  onSlotClick={(slotIdx, laneId) => openDetails(slotIdx, laneId)}
  onSlotDoubleClick={(slotIdx, laneId) => createAppointment(slotIdx, laneId)}
  onContextMenu={(slotIdx, laneId) => showContextMenu(slotIdx, laneId)}
/>

Blocking Overlaps with allowOverlap

The allowOverlap property controls whether OTHER appointments can overlap with this appointment:

// Appointments with allowOverlap: true allow others to be placed over them
const flexibleAppointment = {
  id: "1",
  startSlot: 5,
  duration: 2,
  title: "Flexible Meeting",
  allowOverlap: true, // Other appointments can overlap with this one
};

// Appointments without allowOverlap (default: false) block overlapping
const strictAppointment = {
  id: "2",
  startSlot: 6,
  duration: 2,
  title: "Exclusive Meeting",
  allowOverlap: false, // No other appointments can overlap with this one
};

// When dragging/resizing appointments:
// - They can only overlap with appointments that have allowOverlap: true
// - If they try to overlap with an appointment where allowOverlap: false,
//   the drop/resize is rejected and shown in red

Swapping Appointments

Set collisionStrategy="swap" on Scheduler to exchange two appointments instead of rejecting a drop over a non-overlappable appointment.

<Scheduler
  collisionStrategy="swap"
  onAppointmentMove={(appointment, sourceLaneId, targetLaneId, newStartSlot, details) => {
    if (details.operation === "swap") {
      // Move appointment to targetLaneId/newStartSlot and move
      // details.swappedAppointment to details.swappedAppointmentNewLaneId.
      return;
    }

    // Existing move behavior
  }}
>
  <Lane laneId="room-1" appointments={roomOneAppointments} />
  <Lane laneId="room-2" appointments={roomTwoAppointments} />
</Scheduler>

Swaps are atomic. If the displaced appointment cannot be placed in the dragged appointment's original lane and start slot because of locked, blocked slots, bounds, or other invalid overlaps, the drop is rejected.

Blocked Slots with Custom Logic

const vipAppointment = {
  id: "1",
  startSlot: 5,
  duration: 2,
  onBlockedSlot: (slotIndex, laneId) => {
    // Return true to allow VIP appointments on blocked slots
    return true;
  },
};

Styling

The component uses Tailwind CSS internally. To customize colors and visual configuration:

<Lane
  config={{
    slotColor: "#f9fafb",
    slotBorderColor: "#e5e7eb",
    height: 100,
    slotWidth: 80,
  }}
/>

Custom Element ClassNames

Customize the appearance of appointment containers and resizers with your own CSS classes:

<Lane
  laneId="room-1"
  appointments={appointments}
  appointmentContainerClassName="border-2 border-blue-600 rounded-lg"
  appointmentResizerStartClassName="bg-green-500 opacity-80"
  appointmentResizerEndClassName="bg-red-500 opacity-80"
  appointmentResizerStartInnerClassName="bg-green-700 w-1"
  appointmentResizerEndInnerClassName="bg-red-700 w-1"
/>

Note: Custom classes are intelligently merged with default classes. Classes with conflicting properties (e.g., bg-*, border-*, w-*) will override the defaults while preserving non-conflicting styles.

Each resizer element has two levels:

  • Outer container (appointmentResizerStartClassName/appointmentResizerEndClassName): The larger hitbox for easier grabbing
  • Inner line (appointmentResizerStartInnerClassName/appointmentResizerEndInnerClassName): The visible line within the container

TypeScript

Full TypeScript support included:

import type {
  Appointment,
  LaneConfig,
  LaneProps,
} from "@pangeasi/lane-scheduler";

License

MIT