@lotics/ui 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,616 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { FlatList, ScrollView, StyleSheet, View } from "react-native";
3
+ import { KanbanColumn } from "./kanban_column";
4
+ import { DragPreview } from "./drag_preview";
5
+ import { ColumnPlaceholder } from "./placeholders";
6
+ import {
7
+ CardMoveResult,
8
+ ColumnMoveResult,
9
+ DragInfo,
10
+ DragPreviewInfo,
11
+ DropTarget,
12
+ KanbanColumn as KanbanColumnType,
13
+ KanbanRenderAddCardPlaceholderProps,
14
+ KanbanRenderInsertCardButtonProps,
15
+ KanbanRenderCardProps,
16
+ KanbanRenderCollapsedColumnProps,
17
+ KanbanRenderColumnHeaderProps,
18
+ } from "./types";
19
+ import {
20
+ AUTO_SCROLL_SPEED,
21
+ AUTO_SCROLL_THRESHOLD,
22
+ COLUMN_CONTENT_PADDING,
23
+ DEFAULT_COLUMN_GAP,
24
+ DEFAULT_COLUMN_WIDTH,
25
+ DEFAULT_ITEM_GAP,
26
+ MINIMIZED_COLUMN_WIDTH,
27
+ } from "./constants";
28
+ import {
29
+ defaultRenderAddCardPlaceholder,
30
+ defaultRenderCollapsedColumn,
31
+ defaultRenderColumnHeader,
32
+ } from "./default_renderers";
33
+
34
+ export interface KanbanBoardProps<T> {
35
+ columns: KanbanColumnType<T>[];
36
+ renderCard: (props: KanbanRenderCardProps<T>) => React.ReactNode;
37
+ renderColumnHeader?: (props: KanbanRenderColumnHeaderProps) => React.ReactNode;
38
+ renderAddCardPlaceholder?: (props: KanbanRenderAddCardPlaceholderProps) => React.ReactNode;
39
+ /** Custom renderer for the insert card button shown between cards on hover */
40
+ renderInsertCardButton?: (props: KanbanRenderInsertCardButtonProps) => React.ReactNode;
41
+ renderCollapsedColumn?: (props: KanbanRenderCollapsedColumnProps) => React.ReactNode;
42
+ onCardMove?: (result: CardMoveResult<T>) => void;
43
+ onColumnMove?: (result: ColumnMoveResult) => void;
44
+ onCardPress?: (cardId: string, item: T, columnKey: string) => void;
45
+ onAddCard?: (columnKey: string) => void;
46
+ /** Called when user clicks the insert button between cards */
47
+ onAddCardAtIndex?: (columnKey: string, index: number) => void;
48
+ columnWidth?: number;
49
+ columnGap?: number;
50
+ /** Height of each card. Number for fixed height, function for variable heights */
51
+ itemHeight: number | ((item: T, index: number) => number);
52
+ /** Gap between items in a column */
53
+ itemGap?: number;
54
+ /** When true, cards can only be dropped into columns (highlights entire column), not between specific cards */
55
+ columnOnlyCardDrop?: boolean;
56
+ }
57
+
58
+ /** Column registration data */
59
+ export interface ColumnRegistration {
60
+ element: HTMLElement;
61
+ headerHeight: number;
62
+ /** FlatList ref for scrolling - null when column is minimized */
63
+ listRef: FlatList | null;
64
+ }
65
+
66
+ export function KanbanBoard<T>({
67
+ columns,
68
+ renderCard,
69
+ renderColumnHeader = defaultRenderColumnHeader,
70
+ renderAddCardPlaceholder = defaultRenderAddCardPlaceholder,
71
+ renderInsertCardButton,
72
+ renderCollapsedColumn = defaultRenderCollapsedColumn,
73
+ onCardMove,
74
+ onColumnMove,
75
+ onCardPress,
76
+ onAddCard,
77
+ onAddCardAtIndex,
78
+ columnWidth = DEFAULT_COLUMN_WIDTH,
79
+ columnGap = DEFAULT_COLUMN_GAP,
80
+ itemHeight,
81
+ itemGap = DEFAULT_ITEM_GAP,
82
+ columnOnlyCardDrop = false,
83
+ }: KanbanBoardProps<T>) {
84
+ // Helper to get height for an item
85
+ const getHeight = useCallback(
86
+ (item: T, index: number): number => {
87
+ return typeof itemHeight === "function" ? itemHeight(item, index) : itemHeight;
88
+ },
89
+ [itemHeight],
90
+ );
91
+
92
+ // === State ===
93
+ const [dragInfo, setDragInfo] = useState<DragInfo | null>(null);
94
+ const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
95
+ const [dragPreview, setDragPreview] = useState<DragPreviewInfo | null>(null);
96
+ const [minimizedColumns, setMinimizedColumns] = useState<Set<string>>(new Set());
97
+
98
+ const toggleMinimized = useCallback((columnKey: string) => {
99
+ setMinimizedColumns((prev) => {
100
+ const next = new Set(prev);
101
+ if (next.has(columnKey)) {
102
+ next.delete(columnKey);
103
+ } else {
104
+ next.add(columnKey);
105
+ }
106
+ return next;
107
+ });
108
+ }, []);
109
+
110
+ // === Refs ===
111
+ const dragPositionRef = useRef<{ x: number; y: number } | null>(null);
112
+ const dragPreviewUpdateRef = useRef<((x: number, y: number) => void) | null>(null);
113
+
114
+ // Column data - consolidated into single map
115
+ const columnDataRef = useRef<Map<string, ColumnRegistration & { scrollY: number }>>(new Map());
116
+
117
+ // Track which column mouse is currently over during drag
118
+ const hoveredColumnRef = useRef<string | null>(null);
119
+
120
+ // Refs to avoid stale closures
121
+ const dragInfoRef = useRef<DragInfo | null>(null);
122
+ const dropTargetRef = useRef<DropTarget | null>(null);
123
+ const onCardMoveRef = useRef(onCardMove);
124
+ const onColumnMoveRef = useRef(onColumnMove);
125
+ const columnsRef = useRef(columns);
126
+ const getHeightRef = useRef(getHeight);
127
+ const itemGapRef = useRef(itemGap);
128
+
129
+ // Keep refs in sync
130
+ onCardMoveRef.current = onCardMove;
131
+ onColumnMoveRef.current = onColumnMove;
132
+ columnsRef.current = columns;
133
+ getHeightRef.current = getHeight;
134
+ itemGapRef.current = itemGap;
135
+
136
+ const isDragging = dragInfo !== null;
137
+
138
+ // === Drop Target Detection ===
139
+ const findDropTarget = useCallback(
140
+ (clientX: number, clientY: number, dragType: "card" | "column"): DropTarget | null => {
141
+ if (dragType === "column") {
142
+ const draggedColumnKey = dragInfoRef.current?.id;
143
+ const sourceIndex = dragInfoRef.current?.sourceIndex ?? 0;
144
+
145
+ // The dragged column is not rendered (hidden), so we work with visible columns only
146
+ const visibleColumns = columnsRef.current.filter((c) => c.key !== draggedColumnKey);
147
+
148
+ // Sort by visual position (left edge) to handle any layout differences
149
+ const sortedColumns = [...visibleColumns].sort((a, b) => {
150
+ const dataA = columnDataRef.current.get(a.key);
151
+ const dataB = columnDataRef.current.get(b.key);
152
+ if (!dataA || !dataB) return 0;
153
+ return (
154
+ dataA.element.getBoundingClientRect().left - dataB.element.getBoundingClientRect().left
155
+ );
156
+ });
157
+
158
+ // Find drop position among visible columns using midpoint detection
159
+ let visibleTargetIndex = 0;
160
+ for (let i = 0; i < sortedColumns.length; i++) {
161
+ const data = columnDataRef.current.get(sortedColumns[i].key);
162
+ if (!data) continue;
163
+ const rect = data.element.getBoundingClientRect();
164
+ const midpoint = rect.left + rect.width / 2;
165
+ if (clientX < midpoint) {
166
+ visibleTargetIndex = i;
167
+ break;
168
+ }
169
+ visibleTargetIndex = i + 1;
170
+ }
171
+
172
+ // Map visible index back to full array index
173
+ // Example: columns [A, B, C, D], dragging B (sourceIndex=1)
174
+ // Visible: [A, C, D] at indices [0, 1, 2]
175
+ // If visibleTargetIndex=2 (between C and D), actual index should be 3
176
+ // because B was at index 1, so indices after it shifted down by 1
177
+ const targetIndex =
178
+ visibleTargetIndex > sourceIndex ? visibleTargetIndex + 1 : visibleTargetIndex;
179
+
180
+ return {
181
+ type: "column",
182
+ columnKey:
183
+ columnsRef.current[Math.min(targetIndex, columnsRef.current.length - 1)]?.key ?? "",
184
+ index: targetIndex,
185
+ };
186
+ }
187
+
188
+ // For card drag, detect column from pointer position
189
+ // (more reliable than relying on pointerenter events)
190
+ let columnKey: string | null = null;
191
+ let columnData:
192
+ | (typeof columnDataRef.current extends Map<string, infer V> ? V : never)
193
+ | null = null;
194
+ let rect: DOMRect | null = null;
195
+
196
+ // Find which column contains the pointer
197
+ for (const [key, data] of columnDataRef.current) {
198
+ const columnRect = data.element.getBoundingClientRect();
199
+ if (
200
+ clientX >= columnRect.left &&
201
+ clientX <= columnRect.right &&
202
+ clientY >= columnRect.top &&
203
+ clientY <= columnRect.bottom
204
+ ) {
205
+ columnKey = key;
206
+ columnData = data;
207
+ rect = columnRect;
208
+ break;
209
+ }
210
+ }
211
+
212
+ if (!columnKey || !columnData || !rect) return null;
213
+
214
+ // Update hoveredColumnRef for auto-scroll
215
+ hoveredColumnRef.current = columnKey;
216
+
217
+ // Column-only card drop mode: just return the column, no specific index
218
+ if (columnOnlyCardDrop) {
219
+ return { type: "card", columnKey, index: -1 };
220
+ }
221
+
222
+ // Y position relative to item layout (subtract content padding)
223
+ const relativeY =
224
+ clientY - rect.top - columnData.headerHeight + columnData.scrollY - COLUMN_CONTENT_PADDING;
225
+
226
+ // Find card index based on cumulative heights
227
+ const column = columnsRef.current.find((c) => c.key === columnKey);
228
+ const items = column?.items ?? [];
229
+
230
+ // Get dragging card info
231
+ const draggedCardSourceColumn = dragInfoRef.current?.sourceColumnKey;
232
+ const draggedCardSourceIndex = dragInfoRef.current?.sourceIndex ?? -1;
233
+ const isInSourceColumn = draggedCardSourceColumn === columnKey;
234
+
235
+ let offset = 0;
236
+ for (let i = 0; i < items.length; i++) {
237
+ const height = getHeightRef.current(items[i].data, i);
238
+ const midpoint = offset + height / 2;
239
+ if (relativeY < midpoint) {
240
+ // When in source column, dropping at sourceIndex or sourceIndex+1
241
+ // both mean "keep card in same position", so normalize to sourceIndex
242
+ if (
243
+ isInSourceColumn &&
244
+ (i === draggedCardSourceIndex || i === draggedCardSourceIndex + 1)
245
+ ) {
246
+ return { type: "card", columnKey, index: draggedCardSourceIndex };
247
+ }
248
+ return { type: "card", columnKey, index: i };
249
+ }
250
+ offset += height + itemGapRef.current;
251
+ }
252
+
253
+ // Dropping at the end - same normalization for source column
254
+ if (isInSourceColumn && items.length === draggedCardSourceIndex + 1) {
255
+ return { type: "card", columnKey, index: draggedCardSourceIndex };
256
+ }
257
+ return { type: "card", columnKey, index: items.length };
258
+ },
259
+ [columnOnlyCardDrop],
260
+ );
261
+
262
+ // === Auto-scroll within columns ===
263
+ const performAutoScroll = useCallback((clientX: number, clientY: number) => {
264
+ const columnKey = hoveredColumnRef.current;
265
+ if (!columnKey) return;
266
+
267
+ const columnData = columnDataRef.current.get(columnKey);
268
+ if (!columnData || !columnData.listRef) return; // Skip if minimized (no listRef)
269
+
270
+ const rect = columnData.element.getBoundingClientRect();
271
+ const relativeY = clientY - rect.top - columnData.headerHeight;
272
+ const contentHeight = rect.height - columnData.headerHeight;
273
+
274
+ if (relativeY < AUTO_SCROLL_THRESHOLD) {
275
+ columnData.listRef.scrollToOffset({
276
+ offset: Math.max(0, columnData.scrollY - AUTO_SCROLL_SPEED),
277
+ animated: false,
278
+ });
279
+ } else if (relativeY > contentHeight - AUTO_SCROLL_THRESHOLD) {
280
+ columnData.listRef.scrollToOffset({
281
+ offset: columnData.scrollY + AUTO_SCROLL_SPEED,
282
+ animated: false,
283
+ });
284
+ }
285
+ }, []);
286
+
287
+ // === Drag Position Updates ===
288
+ const updateDragPosition = useCallback(
289
+ (clientX: number, clientY: number) => {
290
+ dragPositionRef.current = { x: clientX, y: clientY };
291
+ dragPreviewUpdateRef.current?.(clientX, clientY);
292
+
293
+ const info = dragInfoRef.current;
294
+ if (!info) return;
295
+
296
+ const target = findDropTarget(clientX, clientY, info.type);
297
+
298
+ // Only update state if target changed
299
+ const prev = dropTargetRef.current;
300
+ if (
301
+ target?.type !== prev?.type ||
302
+ target?.columnKey !== prev?.columnKey ||
303
+ target?.index !== prev?.index
304
+ ) {
305
+ dropTargetRef.current = target;
306
+ setDropTarget(target);
307
+ }
308
+
309
+ if (info.type === "card") {
310
+ performAutoScroll(clientX, clientY);
311
+ }
312
+ },
313
+ [findDropTarget, performAutoScroll],
314
+ );
315
+
316
+ // === End Drag ===
317
+ const endDrag = useCallback(() => {
318
+ const info = dragInfoRef.current;
319
+ const target = dropTargetRef.current;
320
+
321
+ if (info && target) {
322
+ if (info.type === "card" && onCardMoveRef.current) {
323
+ const card = columnsRef.current
324
+ .find((col) => col.key === info.sourceColumnKey)
325
+ ?.items.find((item) => item.id === info.id);
326
+
327
+ if (card) {
328
+ // Adjust index when moving within same column and moving forward
329
+ // Because removing the card shifts all subsequent indices down by 1
330
+ let targetIndex = target.index;
331
+ if (info.sourceColumnKey === target.columnKey && info.sourceIndex < target.index) {
332
+ targetIndex--;
333
+ }
334
+
335
+ onCardMoveRef.current({
336
+ cardId: info.id,
337
+ cardData: card.data,
338
+ sourceColumnKey: info.sourceColumnKey,
339
+ sourceIndex: info.sourceIndex,
340
+ targetColumnKey: target.columnKey,
341
+ targetIndex,
342
+ });
343
+ }
344
+ } else if (info.type === "column" && onColumnMoveRef.current) {
345
+ // Adjust index when moving forward (same reason as cards)
346
+ // findDropTarget returns "placeholder position", we need "final position after removal"
347
+ let targetIndex = target.index;
348
+ if (info.sourceIndex < target.index) {
349
+ targetIndex--;
350
+ }
351
+
352
+ onColumnMoveRef.current({
353
+ columnKey: info.id,
354
+ sourceIndex: info.sourceIndex,
355
+ targetIndex,
356
+ });
357
+ }
358
+ }
359
+
360
+ // Reset all state
361
+ setDragInfo(null);
362
+ setDropTarget(null);
363
+ setDragPreview(null);
364
+ dragInfoRef.current = null;
365
+ dropTargetRef.current = null;
366
+ dragPositionRef.current = null;
367
+ hoveredColumnRef.current = null;
368
+ }, []);
369
+
370
+ // === Global Pointer Listeners ===
371
+ useEffect(() => {
372
+ if (!isDragging) return;
373
+
374
+ const handlePointerMove = (e: PointerEvent) => {
375
+ e.preventDefault();
376
+ updateDragPosition(e.clientX, e.clientY);
377
+ };
378
+
379
+ const handlePointerUp = (e: PointerEvent) => {
380
+ e.preventDefault();
381
+ endDrag();
382
+ };
383
+
384
+ const preventDefault = (e: Event) => e.preventDefault();
385
+
386
+ // Use pointer events only - they handle both mouse and touch
387
+ window.addEventListener("pointermove", handlePointerMove);
388
+ window.addEventListener("pointerup", handlePointerUp);
389
+ window.addEventListener("selectstart", preventDefault);
390
+
391
+ return () => {
392
+ window.removeEventListener("pointermove", handlePointerMove);
393
+ window.removeEventListener("pointerup", handlePointerUp);
394
+ window.removeEventListener("selectstart", preventDefault);
395
+ };
396
+ }, [isDragging, updateDragPosition, endDrag]);
397
+
398
+ // === Start Drag Callbacks ===
399
+ const startCardDrag = useCallback(
400
+ (
401
+ cardId: string,
402
+ columnKey: string,
403
+ index: number,
404
+ startX: number,
405
+ startY: number,
406
+ rect: { x: number; y: number; width: number; height: number },
407
+ ) => {
408
+ const info: DragInfo = {
409
+ type: "card",
410
+ id: cardId,
411
+ sourceColumnKey: columnKey,
412
+ sourceIndex: index,
413
+ };
414
+ setDragInfo(info);
415
+ dragInfoRef.current = info;
416
+ dragPositionRef.current = { x: startX, y: startY };
417
+ hoveredColumnRef.current = columnKey;
418
+ setDragPreview({
419
+ width: rect.width,
420
+ height: rect.height,
421
+ offsetX: startX - rect.x,
422
+ offsetY: startY - rect.y,
423
+ });
424
+ // In column-only mode, use index -1 to show column highlight instead of position indicator
425
+ const initialIndex = columnOnlyCardDrop ? -1 : index;
426
+ setDropTarget({ type: "card", columnKey, index: initialIndex });
427
+ dropTargetRef.current = { type: "card", columnKey, index: initialIndex };
428
+ },
429
+ [columnOnlyCardDrop],
430
+ );
431
+
432
+ const startColumnDrag = useCallback(
433
+ (
434
+ columnKey: string,
435
+ index: number,
436
+ startX: number,
437
+ startY: number,
438
+ rect: { x: number; y: number; width: number; height: number },
439
+ ) => {
440
+ const info: DragInfo = {
441
+ type: "column",
442
+ id: columnKey,
443
+ sourceColumnKey: columnKey,
444
+ sourceIndex: index,
445
+ };
446
+ setDragInfo(info);
447
+ dragInfoRef.current = info;
448
+ dragPositionRef.current = { x: startX, y: startY };
449
+ setDragPreview({
450
+ width: rect.width,
451
+ height: rect.height,
452
+ offsetX: startX - rect.x,
453
+ offsetY: startY - rect.y,
454
+ });
455
+ setDropTarget({ type: "column", columnKey, index });
456
+ dropTargetRef.current = { type: "column", columnKey, index };
457
+ },
458
+ [],
459
+ );
460
+
461
+ // === Column Registration (consolidated) ===
462
+ const registerColumn = useCallback((columnKey: string, data: ColumnRegistration | null) => {
463
+ if (data) {
464
+ const existing = columnDataRef.current.get(columnKey);
465
+ columnDataRef.current.set(columnKey, {
466
+ ...data,
467
+ scrollY: existing?.scrollY ?? 0,
468
+ });
469
+ } else {
470
+ columnDataRef.current.delete(columnKey);
471
+ }
472
+ }, []);
473
+
474
+ const updateColumnScroll = useCallback((columnKey: string, scrollY: number) => {
475
+ const data = columnDataRef.current.get(columnKey);
476
+ if (data) {
477
+ data.scrollY = scrollY;
478
+ }
479
+ }, []);
480
+
481
+ // Kept for potential future use, but column detection now happens in findDropTarget
482
+ const handleColumnPointerEnter = useCallback((columnKey: string) => {
483
+ if (dragInfoRef.current?.type === "card") {
484
+ hoveredColumnRef.current = columnKey;
485
+ }
486
+ }, []);
487
+
488
+ // STABLE: Callback for DragPreview position updates
489
+ const handlePositionUpdate = useCallback((cb: (x: number, y: number) => void) => {
490
+ dragPreviewUpdateRef.current = cb;
491
+ }, []);
492
+
493
+ // === Derived State ===
494
+ const dragType = dragInfo?.type ?? null;
495
+ const dragId = dragInfo?.id ?? null;
496
+ const sourceColumnKey = dragInfo?.sourceColumnKey ?? null;
497
+ const dropTargetType = dropTarget?.type ?? null;
498
+ const dropTargetColumnKey = dropTarget?.columnKey ?? null;
499
+ const dropTargetIndex = dropTarget?.index ?? null;
500
+
501
+ // Width of the column being dragged (for placeholder sizing)
502
+ const draggedColumnWidth =
503
+ dragType === "column" && dragId
504
+ ? minimizedColumns.has(dragId)
505
+ ? MINIMIZED_COLUMN_WIDTH
506
+ : columnWidth
507
+ : columnWidth;
508
+
509
+ // Height of the column being dragged (for placeholder sizing)
510
+ const draggedColumnHeight = dragType === "column" && dragPreview ? dragPreview.height : undefined;
511
+
512
+ // Height of the card being dragged (for placeholder sizing)
513
+ const draggedCardHeight = dragType === "card" && dragPreview ? dragPreview.height : null;
514
+
515
+ // Stable scroll content style
516
+ const scrollContentStyle = useMemo(() => [styles.scrollContent, { gap: columnGap }], [columnGap]);
517
+
518
+ return (
519
+ <View style={styles.container}>
520
+ <ScrollView
521
+ horizontal
522
+ showsHorizontalScrollIndicator={false}
523
+ style={styles.scrollView}
524
+ contentContainerStyle={scrollContentStyle}
525
+ scrollEventThrottle={16}
526
+ >
527
+ {columns.map((column, columnIndex) => {
528
+ const isColumnDragging = dragType === "column" && dragId === column.key;
529
+ const isColumnDropTarget =
530
+ dragType === "column" && dropTargetType === "column" && dropTargetIndex === columnIndex;
531
+
532
+ // Only source column needs cardDragId (to mark dragged card)
533
+ const isSourceColumn = sourceColumnKey === column.key;
534
+ // Only target column needs drop index (to show placeholder)
535
+ const isTargetColumn =
536
+ dragType === "card" && dropTargetType === "card" && dropTargetColumnKey === column.key;
537
+
538
+ const isMinimized = minimizedColumns.has(column.key);
539
+ const effectiveWidth = isMinimized ? MINIMIZED_COLUMN_WIDTH : columnWidth;
540
+
541
+ // In column-only mode, show full column highlight instead of position indicators
542
+ const showCardDropHighlight = columnOnlyCardDrop && isTargetColumn;
543
+
544
+ return (
545
+ <KanbanColumn
546
+ key={column.key}
547
+ columnKey={column.key}
548
+ title={column.title}
549
+ index={columnIndex}
550
+ items={column.items}
551
+ renderCard={renderCard}
552
+ renderColumnHeader={renderColumnHeader}
553
+ renderAddCardPlaceholder={renderAddCardPlaceholder}
554
+ renderInsertCardButton={renderInsertCardButton}
555
+ renderCollapsedColumn={renderCollapsedColumn}
556
+ isDragging={isColumnDragging}
557
+ isDropTarget={isColumnDropTarget}
558
+ isDragInProgress={isDragging}
559
+ // Only pass to source column - other columns don't need to re-render
560
+ cardDragId={isSourceColumn ? dragId : null}
561
+ // Only pass to target column - other columns don't need to re-render
562
+ // Don't pass index in column-only mode (no position indicators)
563
+ cardDropTargetIndex={isTargetColumn && !columnOnlyCardDrop ? dropTargetIndex : null}
564
+ draggedCardHeight={isTargetColumn ? draggedCardHeight : null}
565
+ showCardDropHighlight={showCardDropHighlight}
566
+ columnWidth={effectiveWidth}
567
+ placeholderWidth={isColumnDropTarget ? draggedColumnWidth : effectiveWidth}
568
+ placeholderHeight={isColumnDropTarget ? draggedColumnHeight : undefined}
569
+ itemHeight={itemHeight}
570
+ itemGap={itemGap}
571
+ onCardPress={onCardPress}
572
+ onAddCard={onAddCard}
573
+ onAddCardAtIndex={onAddCardAtIndex}
574
+ startCardDrag={startCardDrag}
575
+ startColumnDrag={startColumnDrag}
576
+ registerColumn={registerColumn}
577
+ updateColumnScroll={updateColumnScroll}
578
+ onPointerEnter={handleColumnPointerEnter}
579
+ isMinimized={isMinimized}
580
+ onMinimizeToggle={toggleMinimized}
581
+ />
582
+ );
583
+ })}
584
+ {dragType === "column" &&
585
+ dropTargetType === "column" &&
586
+ dropTargetIndex === columns.length && (
587
+ <ColumnPlaceholder width={draggedColumnWidth} height={draggedColumnHeight} />
588
+ )}
589
+ </ScrollView>
590
+ <DragPreview
591
+ columns={columns}
592
+ dragInfo={dragInfo}
593
+ dragPreview={dragPreview}
594
+ renderCard={renderCard}
595
+ renderColumnHeader={renderColumnHeader}
596
+ onPositionUpdate={handlePositionUpdate}
597
+ initialPosition={dragPositionRef.current}
598
+ />
599
+ </View>
600
+ );
601
+ }
602
+
603
+ const styles = StyleSheet.create({
604
+ container: {
605
+ flex: 1,
606
+ flexBasis: 0,
607
+ },
608
+ scrollView: {
609
+ flex: 1,
610
+ },
611
+ scrollContent: {
612
+ flexDirection: "row",
613
+ alignItems: "flex-start",
614
+ height: "100%",
615
+ },
616
+ });