npm package: @pangeasi/lane-scheduler-react
Description: Flexible drag-and-drop scheduler component for React
Author: pangeasi
Latest version: 1.1.3

# 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](https://pangeasi.github.io/lane-scheduler-react/)

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

```bash
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:

```tsx
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

```tsx
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 |

### 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

```tsx
// 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.

```tsx
<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:

```tsx
// 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
```

### Blocked Slots with Custom Logic

```tsx
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:

```tsx
<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:

```tsx
<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:

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

## License

MIT