External drops and transfers
Create events from a palette and transfer events between calendars.
External drops create events from outside the calendar. Transfers move an existing event between two calendars. Both use the hit testing, previews and constraints of event editing.
External event sources
On the receiving calendar, register interactionFeature(), pointerFeature() and externalDropFeature(); movement
and selection are optional. onExternalDrop receives { kind: 'create', after, metadata }, so
onExternalDrop: ({ after }) => addEvent(after) inserts the accepted event into your state.
Attach a palette item after its element mounts:
export function attachPalette(
element: HTMLElement,
makeEvent: () => Calendar_EventInput<Booking>,
dragScope: Calendar_DragScope,
) {
return createExternalDraggable(
element,
() => ({
event: makeEvent(),
metadata: { source: 'booking-palette' },
}),
{ dragScope, pointer: {} },
);
}Create a scope with createCalendarDragScope() and pass it to both the receiver,
useCalendarInteractionBindings(calendar, { dragScope }), and the palette. The palette owns its pointer and optional
touch activation settings; the receiver owns its scrolling. Call the returned cleanup on unmount. makeEvent runs
when a gesture begins, so give each new event a unique ID. The event keeps your data type, and the optional metadata
arrives on the proposal.
externalDrop.accept(event, metadata) can reject a source. externalDrop.relocationPolicy overrides fields of the
calendar's eventRelocationPolicy, independently of movement. By default conversion is off and the destination
resource replaces the assignment list. Drops whose ID already exists in the destination are rejected.
For keyboard users, call api.externalDrop.begin(event, metadata?), update the active session with a date target and
commit. Your application still inserts after.
Cross-calendar transfers
Register interactionFeature(), pointerFeature() and eventTransferFeature() on both calendars, and give each a
distinct calendar-level id, such as 'left' and 'right'. Neither calendar references the other. The source also
needs eventMoveFeature(); the receiver needs neither movement nor selection. Settings live under transfer:
enabled, accept and relocationPolicy.
Pass the same { dragScope } to both calendars' bindings; a host without one is isolated. To drag both ways, register
movement and transfer on both, and either calendar can be source or receiver on each gesture.
Dragging an event onto the other calendar holds the source's movement session while the receiver validates the
proposal against its own constraints, its eventRelocationPolicy and transfer.relocationPolicy. The source calendar's
settings don't apply. Duplicate IDs in the destination are rejected. transfer.accept(sourceId) on the receiver
restricts senders.
On commit, the source calls onEventTransfer once with:
({ kind: 'transfer', sourceId, destinationId, before, after });Remove before.id from the source and add after to the destination in one transaction. The library changes neither
array, so keep event state shared between the calendars to avoid applying half a transfer. One callback can serve both
calendars, since each proposal names its arrays:
export function applyTransfer(events: TransferEvents, proposal: Calendar_TransferProposal<Booking>): TransferEvents {
return {
...events,
[proposal.sourceId]: events[proposal.sourceId].filter(event => event.id !== proposal.before.id),
[proposal.destinationId]: [...events[proposal.destinationId], proposal.after],
};
}Programmatic transfers
Programmatic creation and transfer need no pointerFeature. Call api.transfer.prepare(eventId, resourceId?) on the
source, then the receiver's transfer.begin or transfer.evaluate with the handle. Always call the handle's
release() when abandoning a transfer; the DOM driver does this for pointer gestures.
This helper assumes the default internal owner. A controlled owner must acknowledge pending snapshots before the coordinator continues or releases the prepared source.
export function transferEvent(
source: { api: { transfer: Calendar_TransferApi<Booking> } },
destination: { api: { transfer: Calendar_TransferApi<Booking> } },
eventId: string,
target: Calendar_DateTarget,
sourceResourceId?: string,
) {
const prepared = source.api.transfer?.prepare(eventId, sourceResourceId);
if (!prepared) return 'unavailable';
try {
const session = destination.api.transfer?.begin(prepared);
if (session?.status !== 'active') return session?.status ?? 'unavailable';
session.session.update(target);
const result = session.session.commit();
if (result.status !== 'committed') session.session.cancel();
return result;
} finally {
prepared.release();
}
}See editing policies for conversion and resource rules.
Bidirectional React setup
The shared features register interaction, movement, transfer and pointer input. This hook owns both event arrays
and one drag scope. Render each calendar with its own provider, root, surface and event source refs as described in
selection and editing.
export function useTransferCalendars() {
const [dragScope] = useState(createCalendarDragScope);
const [events, setEvents] = useState<TransferEvents>({ left: initialEvents, right: [] });
const [leftDate, setLeftDate] = useState(initialDate);
const [rightDate, setRightDate] = useState(initialDate);
const onEventTransfer = (proposal: Calendar_TransferProposal<Booking>) =>
setEvents(current => applyTransfer(current, proposal));
const left = useCalendar({
id: 'left',
features,
views,
view: 'day',
date: leftDate,
timeZone: 'UTC',
events: events.left,
onEventTransfer,
onNavigate: next => setLeftDate(next.date),
});
const right = useCalendar({
id: 'right',
features,
views,
view: 'day',
date: rightDate,
timeZone: 'UTC',
events: events.right,
onEventTransfer,
onNavigate: next => setRightDate(next.date),
});
const leftBindings = useCalendarInteractionBindings(left, { dragScope });
const rightBindings = useCalendarInteractionBindings(right, { dragScope });
return { left, right, leftBindings, rightBindings };
}