# Árboles de recursos, columnas y selección

> Recursos en árbol, columnas ordenables, redimensionables y reordenables en la cabecera de filas, filtros de filas y eventos, y selección de eventos, filas o rangos.

Source: https://superscheduler.org/es/docs/trees-columns-selection/
Reviewed: 2026-10-07

Activa treeEnabled y anida recursos en children; un padre sigue plegado salvo que tenga expanded: true. Añade columnas a la cabecera de filas con rowHeaderColumns y rellena sus celdas a partir de campos del recurso mediante display. Filtra con control.rows.filter() y control.events.filter() junto con onRowFilter y onEventFilter, y selecciona con eventClickHandling: 'Select', la selección rectangular, rowClickHandling: 'Select' y las APIs multiselect y multirange.

Las planificaciones reales rara vez tienen una lista plana de recursos. Las habitaciones pertenecen a plantas, los técnicos a regiones y las máquinas a líneas. Esta guía trata las funciones de Pro que organizan las filas y permiten trabajar con varios elementos a la vez: árboles de recursos, columnas en la cabecera de filas, filtros y los tres tipos de selección (eventos, filas y rangos de tiempo).

Todo lo que aparece aquí necesita SuperScheduler Pro (`super-scheduler`). La edición Lite pinta una lista plana y rechaza los `children` y las `columns` de los recursos.

## Agrupar recursos en un árbol
Un árbol son datos sin más: un recurso con un array `children` se convierte en una fila padre. Dos cosas lo activan:

- `treeEnabled: true` en el scheduler. Sin esta opción, los `children` se ignoran y la cabecera de filas no tiene botones para plegar y desplegar.
- `expanded: true` en cada padre que deba empezar abierto. Los padres están plegados salvo que `expanded` sea exactamente `true`.

```tsx
// src/TeamPlanner.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

// Regions are parent rows and technicians their children. A parent stays collapsed
// unless its resource says `expanded: true`.
const RESOURCES: SuperScheduler.ResourceData[] = [
  {
    id: 'north',
    name: 'North region',
    expanded: true,
    children: [
      { id: 'ana', name: 'Ana Ruiz' },
      { id: 'ben', name: 'Ben Ortiz' },
    ],
  },
  {
    id: 'south',
    name: 'South region',
    children: [
      { id: 'cai', name: 'Cai Moreno' },
      { id: 'dee', name: 'Dee Patel' },
    ],
  },
]

const JOBS: SuperScheduler.EventData[] = [
  {
    id: 'job-1',
    resource: 'ana',
    start: '2026-10-05T00:00:00',
    end: '2026-10-07T00:00:00',
    text: 'Panel upgrade',
  },
  {
    id: 'job-2',
    resource: 'cai',
    start: '2026-10-06T00:00:00',
    end: '2026-10-09T00:00:00',
    text: 'Boiler service',
  },
]

export function TeamPlanner() {
  const { controlRef, control } = useSchedulerControl()
  // The control adopts the events array and mutates it: hand it a copy.
  const events = useMemo(() => JOBS.slice(), [])

  return (
    <section aria-labelledby="team-plan-title">
      <h2 id="team-plan-title">Field teams</h2>
      <div role="toolbar" aria-label="Rows">
        <button type="button" disabled={control === null} onClick={() => control?.rows.expandAll()}>
          Expand all
        </button>
        <button
          type="button"
          disabled={control === null}
          onClick={() => control?.rows.collapseAll()}
        >
          Collapse all
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        cellWidth={56}
        treeEnabled
        treeIndent={16}
        // Region rows group technicians: no jobs and no time selection on them.
        treePreventParentUsage
        resources={RESOURCES}
        events={events}
      />
    </section>
  )
}
```
Deberías ver «North region» abierta con sus dos técnicos, «South region» cerrada y los dos botones funcionando en cuanto el scheduler se haya montado (hasta entonces, `control` es `null`).

Opciones relacionadas:

| Opción | Por defecto | Efecto |
|---|---|---|
| `treeEnabled` | `false` | Construye las filas a partir de `children`. |
| `treeIndent` | `20` | Sangría por nivel, en píxeles. |
| `treePreventParentUsage` | `false` | Las filas padre no aceptan eventos ni selección de tiempo. |
| `treeAutoExpand` | `true` | Un padre plegado se abre cuando se arrastra un evento sobre él durante medio segundo. |
| `rowFilterParentsAlwaysVisible` | `true` | Mantiene los ancestros de una fila que cumple un filtro. |

Los padres también pueden contener eventos (una reserva para «todo el equipo») salvo que actives `treePreventParentUsage`. Un recurso concreto rechaza los eventos que se sueltan sobre él con `preventUsage: true`, y los recursos de primer nivel se pueden fijar por encima o por debajo de las filas con scroll con `frozen: 'top'` o `frozen: 'bottom'`, algo útil para filas de resumen.

> **Behavior:**
> Cuando una fila se abre o se cierra, el control escribe el nuevo estado en el campo `expanded` de ese objeto de recurso. Si tu aplicación pasa más tarde un array `resources` nuevo, prevalecen los valores de `expanded` de ese array.

### Desplegar y plegar desde código
El control expone el árbol mediante `control.rows`:

- `control.rows.expandAll()` y `control.rows.collapseAll()`;
- `control.rows.expand(level)` abre los padres por encima de un nivel (`1` por defecto, `-1` para todos los niveles);
- `control.rows.find(id)` devuelve una fila; `row.expand()`, `row.collapse()` y `row.toggle()` la cambian.

`onResourceExpand` y `onResourceCollapse` comunican un cambio después de que ocurra, con la fila como `args.resource`. Úsalos para recordar qué ha abierto el usuario o para mantener sincronizadas otras partes de tu interfaz.

> **Limitation:**
> Estos dos handlers son notificaciones: el tipo de sus argumentos incluye `preventDefault()`, pero llamarlo no impide que la fila se pliegue o despliegue. La carga de hijos al abrir un padre (`onLoadNode`) y el arrastre de filas para reordenarlas (`rowMoveHandling`) están reservados y no están implementados. `treeAnimation` se acepta pero no tiene efecto; las filas se abren sin animación.

## Añadir columnas a la cabecera de filas
Sin `rowHeaderColumns`, la cabecera de filas muestra el `name` de cada recurso. Con esta opción, la cabecera se convierte en una pequeña tabla:

- `text` es el título de la columna (también se aceptan `name` y `title`);
- `display` nombra un campo del recurso cuyo valor rellena la celda (antes se busca un campo con ese nombre dentro de `tags`);
- la primera columna muestra el `name` del recurso cuando no tiene `display`;
- `width` fija el ancho en píxeles (por defecto, `rowHeaderColumnDefaultWidth`, 80);
- `sort` hace que el título se pueda pulsar: ordena las filas por ese campo, primero de forma ascendente y después descendente, y pone `aria-sort` en el título.

Un recurso también puede llevar sus propias `columns: [{ text }, { html }]`. Esas entradas se asignan por posición, una por columna.

```tsx
// src/TechnicianColumns.tsx
import { useState } from 'react'
import { type SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import 'super-scheduler/styles.css'

type Status = 'available' | 'off'

const TECHNICIANS: SuperScheduler.ResourceData[] = [
  { id: 'ana', name: 'Ana Ruiz', skill: 'Electrical', shift: 'Early', status: 'available' },
  { id: 'ben', name: 'Ben Ortiz', skill: 'HVAC', shift: 'Late', status: 'off' },
  { id: 'cai', name: 'Cai Moreno', skill: 'Plumbing', shift: 'Early', status: 'available' },
]

// `display` reads a resource field, so a column keeps its values when it is reordered.
// `sort` makes the title clickable: it sorts the rows by that field, ascending then descending.
const INITIAL_COLUMNS: SuperScheduler.RowHeaderColumnData[] = [
  { text: 'Technician', width: 150, sort: 'name' },
  { text: 'Skill', display: 'skill', width: 110, sort: 'skill' },
  { text: 'Shift', display: 'shift', width: 70, nonresizable: true },
  { text: 'Status', display: 'status', width: 100 },
]

const STATUS_LABEL: Record<Status, string> = { available: 'Available', off: 'Day off' }

// Runs with `this` = the control, whose rowHeaderColumns are always in the current order.
function decorateStatus(
  this: SuperScheduler.SchedulerApi,
  args: SuperScheduler.SchedulerBeforeRowHeaderRenderArgs,
): void {
  const index = this.rowHeaderColumns?.findIndex((column) => column.display === 'status') ?? -1
  const cell = args.row.columns[index]
  const status = args.row.data.status
  if (cell === undefined || (status !== 'available' && status !== 'off')) return
  // With rowHeaderColumns, row.html is ignored: write the column's own html (trusted markup).
  cell.html = `<span class="tech-status tech-status--${status}">${STATUS_LABEL[status]}</span>`
}

export function TechnicianColumns({ events }: { events: SuperScheduler.EventData[] }) {
  // Kept in state so a parent re-render never hands the control the original order again.
  const [columns, setColumns] = useState(INITIAL_COLUMNS)

  return (
    <SuperSchedulerComponent
      startDate="2026-10-05"
      days={14}
      scale="Day"
      cellWidth={56}
      resources={TECHNICIANS}
      events={events}
      rowHeaderColumns={columns}
      rowHeaderColumnsReorderable
      onRowHeaderColumnsChange={(args) => setColumns([...args.columns])}
      onBeforeRowHeaderRender={decorateStatus}
    />
  )
}
```
Para decorar una celda, usa `onBeforeRowHeaderRender`. Con columnas, el `html` propio de la fila se ignora: escribe en `args.row.columns[i].html`. Ese marcado es HTML de confianza, así que escapa el texto de los usuarios con `SuperScheduler.Util.escapeHtml`. Para contenido React en la cabecera de filas, consulta [Slots de renderizado React](https://superscheduler.org/es/docs/react-render-slots/).

### Redimensionar y reordenar columnas
Las columnas se pueden redimensionar por defecto (`rowHeaderColumnsResizable: true`); `nonresizable: true` lo desactiva para una columna. Tras redimensionar, el control escribe el nuevo ancho en ese objeto de columna de tu array `rowHeaderColumns` y llama a `onRowHeaderColumnResized({ column })`.

La reordenación está desactivada por defecto. Con `rowHeaderColumnsReorderable`, los usuarios arrastran los títulos de las columnas; los bordes de redimensionado tienen prioridad sobre el arrastre. En `keyboardMode: 'Full'`, Alt+Mayús+flecha izquierda y Alt+Mayús+flecha derecha mueven el título con foco y anuncian el cambio. Desde código, `control.moveRowHeaderColumn(from, to)` mueve una columna según su índice en el orden actual.

Cada redimensionado y cada reordenación llaman a `onRowHeaderColumnsChange` con:

- `columns`: las columnas en su nuevo orden, con sus anchos;
- `order`: `order[i]` es el índice original (en la prop que pasaste) de la columna que ahora está en la posición `i`;
- `widths` y `reason` (`'resize'` o `'reorder'`).

Guarda las columnas en el estado de React, como hace el snippet, y adopta `args.columns`. De lo contrario, un padre que se vuelva a renderizar con un array literal nuevo devolvería el orden original al control.

> **Tip:**
> Rellena las columnas reordenables mediante `display`. Un valor leído de un campo acompaña a su columna; las entradas posicionales `resource.columns[i]` se quedan en su posición. Para conservar los anchos y el orden entre sesiones, guárdalos con las [vistas guardadas](https://superscheduler.org/es/docs/panes-saved-views/).

## Filtrar filas y eventos
Un filtro es un parámetro más un callback. `control.rows.filter(param)` guarda el parámetro y llama a `onRowFilter` para cada fila; pon `args.visible = false` para ocultarla. `control.events.filter(param)` y `onEventFilter` funcionan igual con los eventos. Un parámetro falsy (`''`, `null`, `0`) borra el filtro.

```tsx
// src/FilteredJobs.tsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

type JobStatus = 'scheduled' | 'done' | 'cancelled'
type Job = { status: JobStatus }

// Module-level handlers keep their identity: a new onRowFilter or onEventFilter
// function makes the control evaluate the active filter again.
const FILTERS: Pick<SchedulerProps, 'onRowFilter' | 'onEventFilter'> = {
  onRowFilter: (args) => {
    const query = String(args.filterParam).toLowerCase()
    args.visible = args.row.name.toLowerCase().includes(query)
  },
  onEventFilter: (args) => {
    const job = args.e.data as SuperScheduler.EventData<Job>
    args.visible = job.status === args.filterParam
  },
}

export function FilteredJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const [query, setQuery] = useState('')
  const [status, setStatus] = useState<JobStatus | ''>('')
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // A falsy parameter ('' or null) clears the filter.
  useEffect(() => control?.rows.filter(query.trim() || null), [control, query])
  useEffect(() => control?.events.filter(status || null), [control, status])

  return (
    <>
      <label>
        Technician <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
      </label>
      <label>
        Status
        <select value={status} onChange={(e) => setStatus(e.target.value as JobStatus | '')}>
          <option value="">All</option>
          <option value="scheduled">Scheduled</option>
          <option value="done">Done</option>
          <option value="cancelled">Cancelled</option>
        </select>
      </label>
      <SuperSchedulerComponent
        {...FILTERS}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        treeEnabled
        // Keeps the region row of every matching technician (default true).
        rowFilterParentsAlwaysVisible
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Al escribir «ben» quedan «North region» y «Ben Ortiz»: `rowFilterParentsAlwaysVisible` mantiene los ancestros de cada fila que coincide. Elegir un estado oculta los demás eventos sin tocar las filas.

> **Behavior:**
> El control vuelve a evaluar el filtro activo cuando cambia la función `onRowFilter` u `onEventFilter`. Defínelas a nivel de módulo o memorízalas. La selección de rangos con Mayús+clic se salta las filas ocultas y los eventos filtrados.

Los filtros se ejecutan en el navegador sobre los datos que tiene el control. Una búsqueda en datos que no están cargados (otro mes, otra sede) es una consulta a tu backend, seguida de nuevos `resources` o `events`.

## Seleccionar eventos
Por defecto, un clic en un evento solo ejecuta `onEventClick`. La selección necesita `eventClickHandling: 'Select'`:

- un clic alterna la selección del evento y deselecciona los demás;
- Ctrl+clic (Cmd+clic en macOS) alterna su selección y mantiene los demás, mientras `allowMultiSelect` sea `true` (el valor por defecto);
- con `eventMultiSelectRange`, Mayús+clic selecciona los eventos entre el último evento pulsado y este, a lo largo de las filas visibles; Ctrl/Cmd+Mayús los añade.

`onEventSelect` se ejecuta antes del cambio y puede cancelarlo con `args.preventDefault()`; `onEventSelected` se ejecuta después.

La API `control.multiselect` lee y cambia la selección: `get()`, `add(e)`, `remove(e)`, `clear()`, `isSelected(e)` y `selectAll({ scope, filter })`. `selectAll` selecciona por defecto los eventos visibles; `scope: 'all'` recorre todos los eventos cargados, con un coste proporcional al tamaño del almacén.

```tsx
// src/SelectableJobs.tsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'

type Job = { status: 'scheduled' | 'done' }

export function SelectableJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
  onReschedule: (ids: SuperScheduler.EventId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [count, setCount] = useState(0)
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // Rectangle selections and multiselect calls (selectAll, clear) publish the
  // 'selection' topic; clicks are reported by onEventSelected below.
  useEffect(() => {
    if (control === null) return
    return subscribeScheduler(control, 'selection', (selected) => setCount(selected.length))
  }, [control])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click toggles the event; Ctrl+click (Cmd+click on macOS) keeps the others.
      eventClickHandling: 'Select',
      // Shift+click selects every event between the last clicked one and this one.
      eventMultiSelectRange: true,
      // Shift+drag over the grid selects the events inside the rectangle.
      rectangleSelectHandling: 'EventSelect',
      // With the rectangle: Shift adds to the selection, Alt removes from it.
      rectangleSelectModifiers: true,
      onEventSelected() {
        setCount(this.multiselect.get().length)
      },
      // Marks selected events for CSS. The hook runs again for every event whose
      // selection changes.
      onBeforeEventRender: (args) => {
        const e = args.control.events.find(args.data.id)
        if (e !== null && args.control.multiselect.isSelected(e)) {
          args.data.cssClass = [args.data.cssClass, 'is-selected'].filter(Boolean).join(' ')
        }
      },
    }),
    [],
  )

  const selectScheduled = () =>
    control?.multiselect.selectAll({
      scope: 'view',
      filter: (e) => (e.data as SuperScheduler.EventData<Job>).status === 'scheduled',
    })

  const reschedule = () => {
    const ids = control?.multiselect.get().map((e) => e.id()) ?? []
    if (ids.length > 0) props.onReschedule(ids)
  }

  return (
    <>
      <div role="toolbar" aria-label="Selection">
        <button type="button" onClick={selectScheduled}>
          Select scheduled in view
        </button>
        <button type="button" onClick={() => control?.multiselect.clear()}>
          Clear
        </button>
        <button type="button" disabled={count === 0} onClick={reschedule}>
          Reschedule {count} {count === 1 ? 'job' : 'jobs'}
        </button>
      </div>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Deberías ver cómo el contador del botón sigue los clics, los rectángulos y las acciones de la barra de herramientas, y los trabajos seleccionados resaltados con este CSS:

```css
.super-scheduler__event.is-selected {
  z-index: 2;
  box-shadow: 0 0 0 2px var(--super-scheduler-accent), var(--super-scheduler-shadow-2);
}
```

> **Limitation:**
> En la 0.1.0, un evento seleccionado no recibe ninguna marca visual propia: el estilo de selección de la hoja de estilos apunta a un atributo `data-selected` que los eventos no reciben. Añade una clase en `onBeforeEventRender`, como hace el snippet; el hook se vuelve a ejecutar para cada evento cuya selección cambia. Con el prefijo opcional `theme`, los eventos seleccionados también reciben una clase `{theme}_selected`.

> **Behavior:**
> Los clics se comunican mediante `onEventSelected`. Las selecciones rectangulares y las llamadas a `multiselect` (`selectAll`, `clear`, `add`, `remove`) publican el tema `selection` de `super-scheduler/hooks`. El snippet escucha ambos para que el contador esté siempre al día.

Con `allowMultiMove`, arrastrar un evento seleccionado mueve toda la selección; cada evento pasa por las mismas reglas de movimiento (consulta [arrastrar, redimensionar y reglas de negocio](https://superscheduler.org/es/docs/drag-resize-rules/)). En el modo de teclado completo, Ctrl/Cmd+A selecciona todos los eventos visibles.

### Selección rectangular
`rectangleSelectHandling` decide qué hace un rectángulo trazado con el puntero:

- `'EventSelect'` selecciona los eventos que contiene (y ejecuta los callbacks);
- `'Enabled'` solo ejecuta los callbacks, para aplicaciones que actúan por su cuenta sobre el rectángulo;
- `'Disabled'` (por defecto) lo desactiva.

Los usuarios empiezan un rectángulo con Mayús + arrastrar sobre la cuadrícula. Para empezarlo sin Mayús, pon `args.action = 'RectangleSelect'` en `onGridMouseDown`, o llama a `control.multiselect.startRectangle()` antes de la siguiente pulsación. `rectangleSelectMode: 'Row'` mantiene el rectángulo en la fila donde empezó la pulsación. Con `rectangleSelectModifiers`, Mayús añade a la selección actual y Alt quita de ella; sin ninguna de las dos teclas, el rectángulo sustituye la selección.

Los callbacks son `onRectangleSelecting` (en cada fotograma; `args.visible = false` oculta el rectángulo), `onRectangleSelect` (cancelable, con `events`, `start`, `end` y `resources`) y `onRectangleSelected`.

## Seleccionar filas
`rowClickHandling: 'Select'` hace que un clic en la cabecera de fila seleccione la fila. Ctrl/Cmd alterna la selección de una fila y Mayús selecciona un rango desde la última fila pulsada. Las filas seleccionadas llevan `aria-selected="true"`.

```tsx
// src/RosterRows.tsx
import { useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

export function RosterRows(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onNotify: (resourceIds: SuperScheduler.ResourceId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [selected, setSelected] = useState<readonly SuperScheduler.ResourceId[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click on a row header selects the row; Ctrl/Cmd toggles, Shift selects a range.
      rowClickHandling: 'Select',
      onRowSelected() {
        setSelected(this.rows.selection.get().map((row) => row.id))
      },
    }),
    [],
  )

  return (
    <>
      <button
        type="button"
        disabled={selected.length === 0}
        onClick={() => props.onNotify([...selected])}
      >
        Notify {selected.length} technicians
      </button>
      <button
        type="button"
        onClick={() => {
          control?.rows.selection.clear()
          setSelected([])
        }}
      >
        Clear rows
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
`control.rows.selection` ofrece `get()`, `add(row)`, `remove(row)`, `isSelected(row)` y `clear()`; estas llamadas resaltan las filas al instante y no disparan ningún callback, y por eso el snippet restablece su propio estado después de `clear()`. `onRowSelect` (cancelable) y `onRowSelected` se ejecutan alrededor de cada selección hecha con un clic.

La prop `selectedRows` contiene los ids de los recursos seleccionados. Pásala para empezar con una selección; el control actualiza ese mismo array a medida que cambia la selección.

## Seleccionar rangos de tiempo
Arrastrar sobre celdas vacías selecciona un rango de tiempo y llama a `onTimeRangeSelected` una vez, al soltar, con `start`, `end` (exclusivo), `resource` y `origin: 'drag'`. Otros orígenes:

- `'click'`: un clic simple en una celda vacía selecciona esa celda;
- `'keyboard'`: con el teclado activado, Intro sobre una celda con foco, o Mayús+flecha izquierda/derecha para ampliar un rango;
- `'api'`: `control.selectTimeRange(start, end, resource)`.

Comprueba `args.origin` cuando solo un arrastre deba crear algo. La selección sigue dibujada después de soltar, hasta la siguiente selección o hasta `control.clearSelection()`. Un clic en otro sitio también la borra, salvo con `timeRangeSelectedHandling: 'HoldForever'`; `'Disabled'` desactiva la selección de rangos.

`onTimeRangeSelecting` se ejecuta en cada cambio durante el arrastre. Pon `args.allowed = false` para rechazar un rango (la sombra se marca y al soltar no se selecciona nada), o reescribe `args.start` y `args.end` para acotarlo. Las celdas deshabilitadas y los padres bajo `treePreventParentUsage` rechazan las selecciones sin necesidad de código.

### Varios rangos a la vez
Con `allowMultiRange`, un nuevo arrastre puede añadir un rango en lugar de sustituir el actual: mientras se mantiene pulsado Ctrl/Cmd (`multiRangeMode: 'CtrlOrMeta'`, el valor por defecto) o siempre (`'Always'`). `args.multirange` enumera todos los rangos seleccionados, y `control.multirange` los lee o los cambia: `get()`, `clear()` y `add(new SuperScheduler.Selection(start, end, resource))`.

```tsx
// src/RangeBlocker.tsx
import { useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'

export interface BlockRequest {
  readonly resource: SuperScheduler.ResourceId
  /** ISO wall-clock values; `end` is exclusive. */
  readonly start: string
  readonly end: string
}

export function RangeBlocker(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onBlock: (requests: BlockRequest[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [ranges, setRanges] = useState<readonly SuperScheduler.SchedulerRange[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // Ctrl/Cmd + drag adds a range instead of replacing the current one.
      allowMultiRange: true,
      multiRangeMode: 'CtrlOrMeta',
      onTimeRangeSelecting: (args) => {
        // Past days cannot be blocked: the shadow turns red and the drop is refused.
        args.allowed = args.start.getTime() >= SuperScheduler.Date.today().getTime()
      },
      onTimeRangeSelected: (args) => {
        // A plain click on an empty cell also selects it (origin 'click').
        setRanges([...args.multirange])
      },
    }),
    [],
  )

  const block = () => {
    props.onBlock(
      ranges.map((range) => ({
        resource: range.resource,
        start: range.start.value,
        end: range.end.value,
      })),
    )
    control?.clearSelection()
    setRanges([])
  }

  return (
    <>
      <button type="button" disabled={ranges.length === 0} onClick={block}>
        Block {ranges.length} selected {ranges.length === 1 ? 'range' : 'ranges'}
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Arrastra sobre días libres de una habitación y después de otra manteniendo pulsado Ctrl (Cmd en macOS): el botón muestra «Block 2 selected ranges». Los rangos llegan a `onBlock` como valores ISO de reloj con finales exclusivos, listos para enviarlos a tu backend.

## De qué es dueña tu aplicación
La librería mantiene los árboles, los filtros y las selecciones en el navegador, durante la sesión actual. Tu aplicación decide:

- de dónde sale el árbol: las filas son los `resources` que pasas, y el control nunca pide hijos por su cuenta (`onLoadNode` está reservado);
- qué significa un filtro cuando los datos no están cargados, y cómo llega la búsqueda al backend;
- qué hace una selección: las acciones en bloque, los permisos para ejecutarlas y su persistencia;
- si las filas desplegadas y la disposición de las columnas sobreviven a una recarga (consulta las [vistas guardadas](https://superscheduler.org/es/docs/panes-saved-views/)).

Las selecciones no se guardan en ningún sitio. El estado de despliegue vive en los objetos de recurso que pasaste hasta que los sustituyas.

## Relacionado
→ https://superscheduler.org/es/examples/hotel-rooms/
→ https://superscheduler.org/es/examples/training-rooms/
→ https://superscheduler.org/es/examples/field-service-dispatch/
- [Teclado, accesibilidad y táctil](https://superscheduler.org/es/docs/keyboard-accessibility-touch/) enumera las teclas de selección.
- [Eventos controlados y callbacks](https://superscheduler.org/es/docs/controlled-state/) explica cómo llegan los eventos seleccionados a tu estado.
- [Paneles coordinados y vistas guardadas](https://superscheduler.org/es/docs/panes-saved-views/) conserva las filas plegadas y los anchos de columna.
