react-native-resource-calendar 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1739 @@
1
+ 'use strict';
2
+
3
+ var React18 = require('react');
4
+ var reactNativeGestureHandler = require('react-native-gesture-handler');
5
+ var Animated2 = require('react-native-reanimated');
6
+ var reactNative = require('react-native');
7
+ var flashList = require('@shopify/flash-list');
8
+ var Haptics = require('expo-haptics');
9
+ var dateFnsTz = require('date-fns-tz');
10
+ var dateFns = require('date-fns');
11
+ var lodash = require('lodash');
12
+ var zustand = require('zustand');
13
+ var shallow = require('zustand/shallow');
14
+ var traditional = require('zustand/traditional');
15
+ var reactNativeSkia = require('@shopify/react-native-skia');
16
+ var Svg = require('react-native-svg');
17
+ var native = require('react-content-loader/native');
18
+ var vectorIcons = require('@expo/vector-icons');
19
+
20
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
+
22
+ function _interopNamespace(e) {
23
+ if (e && e.__esModule) return e;
24
+ var n = Object.create(null);
25
+ if (e) {
26
+ Object.keys(e).forEach(function (k) {
27
+ if (k !== 'default') {
28
+ var d = Object.getOwnPropertyDescriptor(e, k);
29
+ Object.defineProperty(n, k, d.get ? d : {
30
+ enumerable: true,
31
+ get: function () { return e[k]; }
32
+ });
33
+ }
34
+ });
35
+ }
36
+ n.default = e;
37
+ return Object.freeze(n);
38
+ }
39
+
40
+ var React18__namespace = /*#__PURE__*/_interopNamespace(React18);
41
+ var Animated2__default = /*#__PURE__*/_interopDefault(Animated2);
42
+ var Haptics__namespace = /*#__PURE__*/_interopNamespace(Haptics);
43
+ var Svg__default = /*#__PURE__*/_interopDefault(Svg);
44
+
45
+ // src/components/Calendar.tsx
46
+ var TIME_LABEL_WIDTH = 50;
47
+ var groupEventsByOverlap = (events) => {
48
+ return events.reduce((clusters, appointment) => {
49
+ const cluster = clusters.find((c) => c.some((e) => isOverlapping(e, appointment)));
50
+ if (cluster) {
51
+ cluster.push(appointment);
52
+ } else {
53
+ clusters.push([appointment]);
54
+ }
55
+ return clusters;
56
+ }, []);
57
+ };
58
+ function computeDisabledBlockColumns(disabledBlocks) {
59
+ const groups = groupDisabledBlocksByOverlap(disabledBlocks);
60
+ const res = /* @__PURE__ */ new Map();
61
+ for (const group of groups) {
62
+ const byStart = [...group].sort((a, b) => a.from - b.from);
63
+ const columns = [];
64
+ for (const evt of byStart) {
65
+ let placed = false;
66
+ for (const col of columns) {
67
+ const last = col[col.length - 1];
68
+ if (!isOverlappingDisabledBlock(last, evt)) {
69
+ col.push(evt);
70
+ placed = true;
71
+ break;
72
+ }
73
+ }
74
+ if (!placed) columns.push([evt]);
75
+ }
76
+ const colIndexByEvent = /* @__PURE__ */ new Map();
77
+ columns.forEach((col, idx) => col.forEach((e) => colIndexByEvent.set(e, idx)));
78
+ const groupCols = columns.length;
79
+ for (const evt of group) {
80
+ const myCol = colIndexByEvent.get(evt);
81
+ let span = 1;
82
+ for (let c = myCol + 1; c < groupCols; c++) {
83
+ const blocked = columns[c].some(
84
+ (e) => e !== evt && isOverlappingDisabledBlock(e, evt)
85
+ );
86
+ if (blocked) break;
87
+ span++;
88
+ }
89
+ const key = evt.id;
90
+ res.set(key, {
91
+ leftIndex: myCol,
92
+ renderColumnCount: groupCols,
93
+ spanColumns: span
94
+ });
95
+ }
96
+ }
97
+ return res;
98
+ }
99
+ var groupDisabledBlocksByOverlap = (disabledBlocks) => {
100
+ return disabledBlocks.reduce((clusters, disabledBlock) => {
101
+ const cluster = clusters.find((c) => c.some((e) => isOverlappingDisabledBlock(e, disabledBlock)));
102
+ if (cluster) {
103
+ cluster.push(disabledBlock);
104
+ } else {
105
+ clusters.push([disabledBlock]);
106
+ }
107
+ return clusters;
108
+ }, []);
109
+ };
110
+ var isOverlappingDisabledBlock = (disabledBlockA, disabledBlockB) => {
111
+ return !(disabledBlockA.to <= disabledBlockB.from || disabledBlockA.from >= disabledBlockB.to);
112
+ };
113
+ function computeEventColumns(events) {
114
+ const groups = groupEventsByOverlap(events);
115
+ const out = /* @__PURE__ */ new Map();
116
+ for (const group of groups) {
117
+ const byStart = [...group].sort(
118
+ (a, b) => a.from - b.from
119
+ );
120
+ const columns = [];
121
+ for (const evt of byStart) {
122
+ let placed = false;
123
+ for (const col of columns) {
124
+ const last = col[col.length - 1];
125
+ if (!isOverlapping(last, evt)) {
126
+ col.push(evt);
127
+ placed = true;
128
+ break;
129
+ }
130
+ }
131
+ if (!placed) columns.push([evt]);
132
+ }
133
+ const colIndex = /* @__PURE__ */ new Map();
134
+ columns.forEach((col, i) => col.forEach((e) => colIndex.set(e, i)));
135
+ const groupCols = columns.length;
136
+ for (const evt of group) {
137
+ const myCol = colIndex.get(evt);
138
+ let span = 1;
139
+ for (let c = myCol + 1; c < groupCols; c++) {
140
+ const blocked = columns[c].some(
141
+ (e) => (
142
+ // exclude self; block if ANY event in col c overlaps me
143
+ e !== evt && e.id !== evt.id && isOverlapping(e, evt)
144
+ )
145
+ );
146
+ if (blocked) break;
147
+ span++;
148
+ }
149
+ out.set(evt.id, {
150
+ leftIndex: myCol,
151
+ renderColumnCount: groupCols,
152
+ spanColumns: span
153
+ });
154
+ }
155
+ }
156
+ return out;
157
+ }
158
+ var getTextSize = (size) => {
159
+ switch (size) {
160
+ case 60:
161
+ return 10;
162
+ case 80:
163
+ return 12;
164
+ case 100:
165
+ return 12;
166
+ default:
167
+ return 12;
168
+ }
169
+ };
170
+ var MINUTES_IN_DAY = 1440;
171
+ var normalizeOvernight = (startMin, endMin) => {
172
+ if (endMin < startMin) endMin += MINUTES_IN_DAY;
173
+ return { startMin, endMin };
174
+ };
175
+ var intersects = (A, B) => Math.max(A.startMin, B.startMin) < Math.min(A.endMin, B.endMin);
176
+ var isOverlapping = (eventA, eventB) => {
177
+ const aStart0 = eventA.from;
178
+ const aEnd0 = eventA.to;
179
+ const bStart0 = eventB.from;
180
+ const bEnd0 = eventB.to;
181
+ if (aStart0 === aEnd0 || bStart0 === bEnd0) return false;
182
+ const A = normalizeOvernight(aStart0, aEnd0);
183
+ const B = normalizeOvernight(bStart0, bEnd0);
184
+ return intersects(A, B);
185
+ };
186
+ var getCurrentTimeInMinutes = (timezone) => {
187
+ const now = dateFnsTz.toZonedTime(/* @__PURE__ */ new Date(), timezone);
188
+ const hours = dateFns.getHours(now);
189
+ const minutes = dateFns.getMinutes(now);
190
+ return hours * 60 + minutes;
191
+ };
192
+ var timeToYPosition = (minutes, TIME_LABEL_HEIGHT) => minutes * (TIME_LABEL_HEIGHT / 60);
193
+ var scalePosition = (position, hourHeight) => {
194
+ return position * (hourHeight / 60);
195
+ };
196
+ var positionToMinutes = (position, TIME_LABEL_HEIGHT) => {
197
+ "worklet";
198
+ return position / (TIME_LABEL_HEIGHT / 60);
199
+ };
200
+ var combineDateAndTime = (date, time) => {
201
+ const [hours, minutes, seconds] = time.split(":").map(Number);
202
+ const combinedDate = dateFns.setSeconds(dateFns.setMinutes(dateFns.setHours(date, hours), minutes), seconds);
203
+ return dateFns.format(combinedDate, "yyyy-MM-dd HH:mm:ss");
204
+ };
205
+ var indexToDate = (index) => {
206
+ const dateWithHour = dateFns.set(/* @__PURE__ */ new Date(), { hours: index, minutes: 0, seconds: 0, milliseconds: 0 });
207
+ return dateFns.format(dateWithHour, "h:mm a");
208
+ };
209
+ var minutesToTime = (totalMinutes) => {
210
+ "worklet";
211
+ const safeTotalMinutes = Math.max(0, Math.round(totalMinutes));
212
+ const hours24 = Math.floor(safeTotalMinutes / 60);
213
+ const minutes = safeTotalMinutes % 60;
214
+ const paddedMins = minutes < 10 ? "0" + minutes : String(minutes);
215
+ const hours12 = hours24 % 12 || 12;
216
+ return `${hours12}:${paddedMins}`;
217
+ };
218
+ function computeStackedEventLayout(events, containerWidthPx, {
219
+ indentPx = 12,
220
+ // how much to nudge each overlap to the right
221
+ rightPadPx = 0,
222
+ // visual breathing room on the right
223
+ minWidthPx = 30,
224
+ // never let an event become thinner than this
225
+ capIndentLevels = 4
226
+ // after N levels, stop indenting (just stack via z-index)
227
+ } = {}) {
228
+ const groups = groupEventsByOverlap(events);
229
+ const out = /* @__PURE__ */ new Map();
230
+ for (const group of groups) {
231
+ const byStart = [...group].sort((a, b) => a.from - b.from);
232
+ const active = [];
233
+ const removeFinished = (currentFrom) => {
234
+ for (let i = active.length - 1; i >= 0; i--) {
235
+ if (!isOverlapping(active[i].e, { ...active[i].e, from: currentFrom, to: currentFrom })) {
236
+ if (active[i].e.to <= currentFrom) active.splice(i, 1);
237
+ }
238
+ }
239
+ };
240
+ const findLowestFreeLevel = () => {
241
+ const used = new Set(active.map((a) => a.level));
242
+ let lvl = 0;
243
+ while (used.has(lvl)) lvl++;
244
+ return lvl;
245
+ };
246
+ for (const e of byStart) {
247
+ removeFinished(e.from);
248
+ let level = findLowestFreeLevel();
249
+ const visualLevel = Math.min(level, capIndentLevels);
250
+ active.push({ e, level });
251
+ const leftPx = visualLevel * indentPx;
252
+ const available = containerWidthPx - leftPx - rightPadPx;
253
+ const widthPx = Math.max(minWidthPx, available);
254
+ const zIndex = 9999 + e.from * 10 + level;
255
+ out.set(e.id, { leftPx, widthPx, zIndex });
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ function columnsToPixels(columnMap, containerWidthPx, {
261
+ gutterPx = 2,
262
+ // spacing between columns
263
+ padLeftPx = 0,
264
+ padRightPx = 0
265
+ } = {}) {
266
+ const out = /* @__PURE__ */ new Map();
267
+ for (const [id, c] of columnMap) {
268
+ const totalGutters = (c.renderColumnCount - 1) * gutterPx;
269
+ const innerWidth = containerWidthPx - padLeftPx - padRightPx - totalGutters;
270
+ const colWidth = innerWidth / c.renderColumnCount;
271
+ const left = padLeftPx + c.leftIndex * (colWidth + gutterPx);
272
+ const width = colWidth * c.spanColumns + gutterPx * (c.spanColumns - 1);
273
+ out.set(id, {
274
+ leftPx: left,
275
+ widthPx: Math.max(0, width),
276
+ // later columns on top slightly, but mostly rely on time-based z
277
+ zIndex: 1e3 + c.leftIndex
278
+ });
279
+ }
280
+ return out;
281
+ }
282
+ function computeEventFrames(events, containerWidthPx, mode, options) {
283
+ if (mode === "columns") {
284
+ const columnLayouts = computeEventColumns(events);
285
+ return columnsToPixels(columnLayouts, containerWidthPx, {
286
+ gutterPx: options?.gutterPx,
287
+ padLeftPx: options?.padLeftPx,
288
+ padRightPx: options?.padRightPx
289
+ });
290
+ } else {
291
+ return computeStackedEventLayout(events, containerWidthPx, {
292
+ indentPx: options?.indentPx,
293
+ rightPadPx: options?.rightPadPx,
294
+ minWidthPx: options?.minWidthPx,
295
+ capIndentLevels: options?.capIndentLevels
296
+ });
297
+ }
298
+ }
299
+ var Col = ({ children, divider, space, style }) => {
300
+ return /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: [{ flexDirection: "column" }, style] }, React18__namespace.default.Children.toArray(children).map((child, index) => /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, { key: index }, child, index !== React18__namespace.default.Children.toArray(children).length - 1 && divider, index !== React18__namespace.default.Children.toArray(children).length - 1 && /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: { height: space, width: "100%" } }))));
301
+ };
302
+ var Col_default = Col;
303
+
304
+ // src/theme/resolveFont.ts
305
+ var WEIGHT_NAME_MAP = {
306
+ "100": "Thin",
307
+ "200": "ExtraLight",
308
+ "300": "Light",
309
+ "400": "Regular",
310
+ "500": "Medium",
311
+ "600": "SemiBold",
312
+ "700": "Bold",
313
+ "800": "ExtraBold",
314
+ "900": "Black"
315
+ };
316
+ function resolveFont({ family = "System", weight = "400", italic = false }) {
317
+ if (family === "System" || family.includes("_")) return family;
318
+ const weightName = WEIGHT_NAME_MAP[weight] ?? "Regular";
319
+ const base = `${family}_${weight}${weightName}`;
320
+ return italic ? `${base}_Italic` : base;
321
+ }
322
+
323
+ // src/theme/ThemeContext.tsx
324
+ var defaultTheme = {
325
+ typography: {
326
+ fontFamily: "System"
327
+ }
328
+ };
329
+ var ThemeCtx = React18.createContext(defaultTheme);
330
+ var useCalendarTheme = () => React18.useContext(ThemeCtx);
331
+ var useResolvedFont = (overrides) => {
332
+ const { typography } = useCalendarTheme();
333
+ const family = overrides?.fontFamily ?? typography?.fontFamily ?? "System";
334
+ const weight = overrides?.fontWeight ?? "400";
335
+ const italic = overrides?.italic ?? false;
336
+ return resolveFont({ family, weight, italic });
337
+ };
338
+ var CalendarThemeProvider = ({ theme, children }) => {
339
+ const mergedTheme = {
340
+ ...defaultTheme,
341
+ ...theme,
342
+ typography: { ...defaultTheme.typography, ...theme?.typography }
343
+ };
344
+ return /* @__PURE__ */ React18__namespace.default.createElement(ThemeCtx.Provider, { value: mergedTheme }, children);
345
+ };
346
+
347
+ // src/components/TimeLabels.tsx
348
+ var TimeLabels = React18__namespace.forwardRef(({
349
+ timezone,
350
+ hourHeight = 120,
351
+ startMinutes = 0,
352
+ totalTimelineWidth,
353
+ date,
354
+ layout
355
+ }, ref) => {
356
+ const isToday = dateFns.isSameDay(/* @__PURE__ */ new Date(), date);
357
+ const [currentTimeYPosition, setCurrentTimeYPosition] = React18.useState(timeToYPosition(getCurrentTimeInMinutes(timezone), hourHeight));
358
+ const [currentTime, setCurrentTime] = React18.useState(dateFns.format(dateFnsTz.toZonedTime(/* @__PURE__ */ new Date(), timezone), "h:mm"));
359
+ const APPOINTMENT_BLOCK_HEIGHT = hourHeight / 4;
360
+ const updateCurrentTimeYPosition = () => {
361
+ setCurrentTimeYPosition(timeToYPosition(getCurrentTimeInMinutes(timezone), hourHeight));
362
+ };
363
+ const updateCurrentTime = () => {
364
+ setCurrentTime(dateFns.format(dateFnsTz.toZonedTime(/* @__PURE__ */ new Date(), timezone), "h:mm"));
365
+ };
366
+ const titleFace = useResolvedFont({ fontWeight: "700" });
367
+ React18.useEffect(() => {
368
+ const update = () => {
369
+ updateCurrentTime();
370
+ updateCurrentTimeYPosition();
371
+ };
372
+ update();
373
+ const intervalId = setInterval(update, 300);
374
+ return () => clearInterval(intervalId);
375
+ }, [timezone]);
376
+ React18.useEffect(() => {
377
+ reactNative.InteractionManager.runAfterInteractions(() => {
378
+ let pos = isToday ? currentTimeYPosition - 240 : timeToYPosition(startMinutes, hourHeight);
379
+ if (ref.current) {
380
+ ref.current.scrollTo({
381
+ y: Math.round(pos / APPOINTMENT_BLOCK_HEIGHT) * APPOINTMENT_BLOCK_HEIGHT,
382
+ // Offset by 240px to give a little margin above the red line
383
+ animated: true
384
+ });
385
+ }
386
+ });
387
+ }, [date, isToday, APPOINTMENT_BLOCK_HEIGHT, startMinutes, hourHeight]);
388
+ return /* @__PURE__ */ React18__namespace.createElement(React18__namespace.Fragment, null, /* @__PURE__ */ React18__namespace.createElement(Col_default, null, Array.from({ length: 24 }).map((_, index) => /* @__PURE__ */ React18__namespace.createElement(reactNative.View, { key: index, style: [styles.timeLabel, { height: hourHeight }] }, /* @__PURE__ */ React18__namespace.createElement(
389
+ reactNative.Text,
390
+ {
391
+ allowFontScaling: false,
392
+ style: { textAlign: "center", fontFamily: titleFace, fontSize: getTextSize(hourHeight) }
393
+ },
394
+ indexToDate(index).split(" ")[0]
395
+ ), /* @__PURE__ */ React18__namespace.createElement(
396
+ reactNative.Text,
397
+ {
398
+ allowFontScaling: false,
399
+ style: { textAlign: "center", fontFamily: titleFace, fontSize: getTextSize(hourHeight) }
400
+ },
401
+ indexToDate(index).split(" ")[1]
402
+ ))), isToday && /* @__PURE__ */ React18__namespace.createElement(reactNative.View, { style: [styles.currentTime, {
403
+ top: currentTimeYPosition - 13,
404
+ width: TIME_LABEL_WIDTH
405
+ }] }, /* @__PURE__ */ React18__namespace.createElement(
406
+ reactNative.Text,
407
+ {
408
+ allowFontScaling: false,
409
+ style: {
410
+ textAlign: "center",
411
+ fontFamily: titleFace,
412
+ fontSize: getTextSize(hourHeight),
413
+ color: "red"
414
+ }
415
+ },
416
+ currentTime
417
+ ))), isToday && /* @__PURE__ */ React18__namespace.createElement(reactNative.View, { style: [styles.currentTimeLine, {
418
+ pointerEvents: "none",
419
+ top: currentTimeYPosition,
420
+ width: totalTimelineWidth,
421
+ left: TIME_LABEL_WIDTH
422
+ }] }));
423
+ });
424
+ var styles = reactNative.StyleSheet.create({
425
+ timeLabel: {
426
+ width: TIME_LABEL_WIDTH
427
+ },
428
+ currentTimeLine: {
429
+ position: "absolute",
430
+ height: 2,
431
+ // Thickness of the line
432
+ backgroundColor: "red",
433
+ zIndex: 1e4
434
+ // Ensure it's on top of all other elements
435
+ },
436
+ currentTime: {
437
+ backgroundColor: "#fff",
438
+ borderColor: "red",
439
+ alignItems: "center",
440
+ justifyContent: "center",
441
+ borderWidth: 2,
442
+ borderRadius: 20,
443
+ height: 26,
444
+ position: "absolute",
445
+ zIndex: 1e4
446
+ // Ensure it's on top of all other elements
447
+ }
448
+ });
449
+ var Hidden = ({ isHidden, children }) => {
450
+ if (isHidden) {
451
+ return null;
452
+ }
453
+ return /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, null, children);
454
+ };
455
+ var Hidden_default = Hidden;
456
+ var Center = ({ children, style }) => {
457
+ return /* @__PURE__ */ React18__namespace.default.createElement(
458
+ reactNative.View,
459
+ {
460
+ style: [{
461
+ justifyContent: "center",
462
+ alignItems: "center"
463
+ }, style]
464
+ },
465
+ children
466
+ );
467
+ };
468
+ var Center_default = Center;
469
+ var Badge = ({
470
+ style,
471
+ value = "",
472
+ children,
473
+ fontSize,
474
+ color = "red",
475
+ textColor = "white"
476
+ }) => {
477
+ const titleFace = useResolvedFont({ fontWeight: "600" });
478
+ return /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: [styles2.badge, { backgroundColor: color }, style] }, children ? children : /* @__PURE__ */ React18__namespace.default.createElement(
479
+ reactNative.Text,
480
+ {
481
+ allowFontScaling: false,
482
+ style: {
483
+ color: textColor,
484
+ fontSize,
485
+ fontFamily: titleFace
486
+ }
487
+ },
488
+ value
489
+ ));
490
+ };
491
+ var styles2 = reactNative.StyleSheet.create({
492
+ badge: {
493
+ justifyContent: "center",
494
+ alignItems: "center",
495
+ borderRadius: 999,
496
+ paddingHorizontal: 6
497
+ }
498
+ });
499
+ var Badge_default = Badge;
500
+ var createCalendarStore = () => zustand.createStore((set2) => ({
501
+ resourcesById: {},
502
+ eventsByResource: {},
503
+ disabledBlocksByResource: {},
504
+ disabledIntervalsByResource: {},
505
+ selectedEvent: null,
506
+ draggedEventDraft: null,
507
+ setSelectedEvent: (evt) => set2({ selectedEvent: evt }),
508
+ upsertResources: (rs) => set2((s) => {
509
+ const next = { ...s.resourcesById };
510
+ let changed = false;
511
+ for (const r of rs) {
512
+ const prev = next[r.id];
513
+ if (!prev || prev.name !== r.name || prev.avatar !== r.avatar) {
514
+ next[r.id] = { id: r.id, name: r.name, avatar: r.avatar };
515
+ changed = true;
516
+ }
517
+ }
518
+ return changed ? { resourcesById: next } : {};
519
+ }),
520
+ setDayData: ({ events, disabledBlocks, disableIntervals }) => set2((s) => ({
521
+ eventsByResource: events ?? s.eventsByResource,
522
+ disabledBlocksByResource: disabledBlocks ?? s.disabledBlocksByResource,
523
+ disabledIntervalsByResource: disableIntervals ?? s.disabledIntervalsByResource
524
+ })),
525
+ setDraggedEventDraft: (draft) => set2({ draggedEventDraft: draft }),
526
+ clearDay: () => set2({
527
+ eventsByResource: {},
528
+ disabledBlocksByResource: {},
529
+ disabledIntervalsByResource: {}
530
+ })
531
+ }));
532
+ var StoreContext = React18.createContext(null);
533
+ var Provider = ({ children }) => {
534
+ const ref = React18.useRef(void 0);
535
+ if (!ref.current) ref.current = createCalendarStore();
536
+ return /* @__PURE__ */ React18__namespace.default.createElement(StoreContext.Provider, { value: ref.current }, children);
537
+ };
538
+ var useBound = (selector, eq) => {
539
+ const store = React18.useContext(StoreContext);
540
+ if (!store) throw new Error("Calendar store used outside of Provider");
541
+ return traditional.useStoreWithEqualityFn(store, selector, eq);
542
+ };
543
+ var useResourceById = (id) => useBound((s) => s.resourcesById[id]);
544
+ var useGetSelectedEvent = () => useBound((s) => s.selectedEvent);
545
+ var useSetSelectedEvent = () => useBound((s) => s.setSelectedEvent);
546
+ var useEventsFor = (resourceId) => useBound((s) => s.eventsByResource[resourceId] ?? [], shallow.shallow);
547
+ var useGetDraggedEventDraft = () => useBound((s) => s.draggedEventDraft);
548
+ var useDisabledBlocksFor = (resourceId) => useBound((s) => s.disabledBlocksByResource[resourceId] ?? [], shallow.shallow);
549
+ var useDisabledIntervalsFor = (resourceId) => useBound((s) => s.disabledIntervalsByResource[resourceId] ?? [], shallow.shallow);
550
+ var useUpsertResources = () => useBound((s) => s.upsertResources);
551
+ var useSetDayData = () => useBound((s) => s.setDayData);
552
+ var useSetDraggedEventDraft = () => useBound((s) => s.setDraggedEventDraft);
553
+ var zustandBinding = {
554
+ Provider,
555
+ useResourceById,
556
+ useEventsFor,
557
+ useDisabledBlocksFor,
558
+ useDisabledIntervalsFor,
559
+ useUpsertResources,
560
+ useSetDayData,
561
+ useGetSelectedEvent,
562
+ useSetSelectedEvent,
563
+ useGetDraggedEventDraft,
564
+ useSetDraggedEventDraft
565
+ };
566
+
567
+ // src/store/bindings/BindingProvider.tsx
568
+ var BindingCtx = React18.createContext(null);
569
+ var useCalendarBinding = () => {
570
+ const ctx = React18.useContext(BindingCtx);
571
+ if (!ctx) throw new Error("useCalendarBinding must be used within <CalendarBindingProvider>");
572
+ return ctx;
573
+ };
574
+ var CalendarBindingProvider = ({ binding, children }) => {
575
+ const active = binding ?? zustandBinding;
576
+ const StoreProvider = active.Provider;
577
+ return /* @__PURE__ */ React18__namespace.default.createElement(BindingCtx.Provider, { value: active }, /* @__PURE__ */ React18__namespace.default.createElement(StoreProvider, null, children));
578
+ };
579
+
580
+ // src/components/ResourcesComponent.tsx
581
+ var ResourceComponent = ({ id, onResourcePress, APPOINTMENT_BLOCK_WIDTH }) => {
582
+ const { useResourceById: useResourceById2, useEventsFor: useEventsFor2 } = useCalendarBinding();
583
+ const resource = useResourceById2(id);
584
+ const events = useEventsFor2(id);
585
+ const titleFace = useResolvedFont({ fontWeight: "700" });
586
+ return /* @__PURE__ */ React18__namespace.createElement(Col_default, { style: [{
587
+ alignItems: "center",
588
+ width: APPOINTMENT_BLOCK_WIDTH
589
+ }] }, /* @__PURE__ */ React18__namespace.createElement(reactNative.View, { style: { position: "relative" } }, /* @__PURE__ */ React18__namespace.createElement(
590
+ StaffAvatar,
591
+ {
592
+ onPress: () => {
593
+ if (onResourcePress)
594
+ onResourcePress(resource);
595
+ },
596
+ name: resource?.name,
597
+ circleSize: 40,
598
+ fontSize: 16,
599
+ badge: events?.length,
600
+ image: resource?.avatar
601
+ }
602
+ )), /* @__PURE__ */ React18__namespace.createElement(
603
+ reactNative.Text,
604
+ {
605
+ style: {
606
+ fontSize: 14,
607
+ fontFamily: titleFace
608
+ },
609
+ numberOfLines: 1,
610
+ allowFontScaling: false
611
+ },
612
+ resource?.name
613
+ ));
614
+ };
615
+ var ResourcesComponent = ({ resourceIds, onResourcePress, APPOINTMENT_BLOCK_WIDTH }) => {
616
+ return /* @__PURE__ */ React18__namespace.createElement(React18__namespace.Fragment, null, resourceIds?.map((id) => {
617
+ return /* @__PURE__ */ React18__namespace.createElement(
618
+ ResourceComponent,
619
+ {
620
+ key: id,
621
+ id,
622
+ APPOINTMENT_BLOCK_WIDTH,
623
+ onResourcePress
624
+ }
625
+ );
626
+ }));
627
+ };
628
+ function StaffAvatar({
629
+ name,
630
+ circleSize = 60,
631
+ fontSize = 36,
632
+ image,
633
+ badge,
634
+ badgeStyle,
635
+ onPress,
636
+ containerStyle,
637
+ ringColor = "#DAEEE7",
638
+ avatarColor,
639
+ textColor
640
+ }) {
641
+ const titleFace = useResolvedFont({ fontWeight: "700" });
642
+ return /* @__PURE__ */ React18__namespace.createElement(
643
+ reactNative.TouchableOpacity,
644
+ {
645
+ disabled: lodash.isUndefined(onPress),
646
+ onPress,
647
+ style: containerStyle
648
+ },
649
+ /* @__PURE__ */ React18__namespace.createElement(Center_default, { style: {
650
+ borderRadius: 9999,
651
+ backgroundColor: ringColor
652
+ } }, /* @__PURE__ */ React18__namespace.createElement(Hidden_default, { isHidden: lodash.isUndefined(badge) || Number(badge) == 0 }, /* @__PURE__ */ React18__namespace.createElement(
653
+ reactNative.View,
654
+ {
655
+ style: [{
656
+ zIndex: 1,
657
+ position: "absolute",
658
+ right: -4,
659
+ top: -6,
660
+ borderRadius: 999,
661
+ backgroundColor: "#fff",
662
+ padding: 4
663
+ }, badgeStyle]
664
+ },
665
+ /* @__PURE__ */ React18__namespace.createElement(
666
+ Badge_default,
667
+ {
668
+ fontSize: 12,
669
+ value: badge + "",
670
+ color: "#4d959c"
671
+ }
672
+ )
673
+ )), /* @__PURE__ */ React18__namespace.createElement(Center_default, { style: {
674
+ margin: 2,
675
+ borderRadius: 9999,
676
+ backgroundColor: "white"
677
+ } }, /* @__PURE__ */ React18__namespace.createElement(Center_default, { style: {
678
+ margin: 2,
679
+ borderRadius: 9999,
680
+ height: circleSize,
681
+ width: circleSize,
682
+ backgroundColor: avatarColor || "#C9E5E8",
683
+ overflow: "hidden"
684
+ } }, image ? /* @__PURE__ */ React18__namespace.createElement(
685
+ reactNative.Image,
686
+ {
687
+ resizeMode: "cover",
688
+ source: { uri: image },
689
+ style: {
690
+ height: "100%",
691
+ borderRadius: 6,
692
+ ...reactNative.StyleSheet.absoluteFillObject
693
+ }
694
+ }
695
+ ) : /* @__PURE__ */ React18__namespace.createElement(
696
+ reactNative.Text,
697
+ {
698
+ allowFontScaling: false,
699
+ style: {
700
+ fontFamily: titleFace,
701
+ fontSize,
702
+ color: textColor || "#4d959c"
703
+ }
704
+ },
705
+ name ? name.split(" ").map((n) => n[0]).join("") : ""
706
+ ))))
707
+ );
708
+ }
709
+ var EventGridBlocksSkia = ({
710
+ dateRef,
711
+ handleBlockPress,
712
+ hourHeight,
713
+ APPOINTMENT_BLOCK_WIDTH
714
+ }) => {
715
+ const rowHeight = hourHeight / 4;
716
+ const [pressedRow, setPressedRow] = React18__namespace.useState(null);
717
+ const timeLabels = React18.useMemo(() => {
718
+ const out = [];
719
+ for (let h = 0; h < 24; h++) {
720
+ for (let q = 0; q < 4; q++) {
721
+ const m = q * 15;
722
+ const hh = String(h).padStart(2, "0");
723
+ const mm = String(m).padStart(2, "0");
724
+ out.push(`${hh}:${mm}:00`);
725
+ }
726
+ }
727
+ return out;
728
+ }, []);
729
+ const rects = React18.useMemo(
730
+ () => timeLabels.map((_, row) => ({
731
+ x: 0,
732
+ y: row * rowHeight,
733
+ width: APPOINTMENT_BLOCK_WIDTH,
734
+ height: rowHeight,
735
+ row
736
+ })),
737
+ [timeLabels, rowHeight, APPOINTMENT_BLOCK_WIDTH]
738
+ );
739
+ const midIndex = Math.ceil(rects.length / 2);
740
+ const firstRects = rects.slice(0, midIndex);
741
+ const secondRects = rects.slice(midIndex);
742
+ const segmentHeight = rowHeight * firstRects.length;
743
+ const onSlotPress = React18__namespace.useCallback(
744
+ (row) => {
745
+ setPressedRow(null);
746
+ const slot = timeLabels[row];
747
+ if (slot) {
748
+ const timestamp = combineDateAndTime(dateRef.current, slot);
749
+ handleBlockPress(timestamp);
750
+ }
751
+ },
752
+ [dateRef, handleBlockPress, timeLabels]
753
+ );
754
+ const onPressBegin = React18__namespace.useCallback((row) => {
755
+ setPressedRow(row);
756
+ }, []);
757
+ const onTouchesUp = React18__namespace.useCallback(() => {
758
+ setPressedRow(null);
759
+ }, []);
760
+ const longPressGesture = reactNativeGestureHandler.Gesture.LongPress().onBegin((e) => Animated2.runOnJS(onPressBegin)(Math.floor(e.y / rowHeight))).onEnd((e) => Animated2.runOnJS(onSlotPress)(Math.floor(e.y / rowHeight))).onFinalize(() => Animated2.runOnJS(onTouchesUp)());
761
+ return /* @__PURE__ */ React18__namespace.createElement(reactNativeGestureHandler.GestureDetector, { gesture: longPressGesture }, /* @__PURE__ */ React18__namespace.createElement(reactNative.View, null, /* @__PURE__ */ React18__namespace.createElement(reactNativeSkia.Canvas, { style: { width: APPOINTMENT_BLOCK_WIDTH, height: segmentHeight } }, firstRects.map(({ x, y, width: w, height: h, row }, idx) => /* @__PURE__ */ React18__namespace.createElement(React18__namespace.Fragment, { key: idx }, /* @__PURE__ */ React18__namespace.createElement(
762
+ reactNativeSkia.Rect,
763
+ {
764
+ x,
765
+ y,
766
+ width: w,
767
+ height: h,
768
+ color: pressedRow === row ? "rgba(240,240,240,0.3)" : "rgba(240,240,240,0.6)",
769
+ style: "fill"
770
+ }
771
+ ), /* @__PURE__ */ React18__namespace.createElement(reactNativeSkia.Line, { p1: { x, y: y + h }, p2: { x: x + w, y: y + h }, color: "#ddd", strokeWidth: 1 })))), /* @__PURE__ */ React18__namespace.createElement(reactNativeSkia.Canvas, { style: { width: APPOINTMENT_BLOCK_WIDTH, height: segmentHeight } }, secondRects.map(({ x, y, width: w, height: h, row }, idx) => /* @__PURE__ */ React18__namespace.createElement(React18__namespace.Fragment, { key: idx }, /* @__PURE__ */ React18__namespace.createElement(
772
+ reactNativeSkia.Rect,
773
+ {
774
+ x,
775
+ y: y - segmentHeight,
776
+ width: w,
777
+ height: h,
778
+ color: pressedRow === row ? "rgba(240,240,240,0.3)" : "rgba(240,240,240,0.6)",
779
+ style: "fill"
780
+ }
781
+ ), /* @__PURE__ */ React18__namespace.createElement(
782
+ reactNativeSkia.Line,
783
+ {
784
+ p1: { x, y: y - segmentHeight + h },
785
+ p2: { x: x + w, y: y - segmentHeight + h },
786
+ color: "#ddd",
787
+ strokeWidth: 1
788
+ }
789
+ ))))));
790
+ };
791
+ var StoreFeeder = ({ store, resources }) => {
792
+ const upsertResources = store.useUpsertResources();
793
+ const setDayData = store.useSetDayData();
794
+ React18.useEffect(() => {
795
+ upsertResources(resources.map((r) => ({ id: r.id, name: r.name, avatar: r.avatar })));
796
+ const eventsByResource = {};
797
+ const blocksByResource = {};
798
+ const intervalsByResource = {};
799
+ for (const r of resources) {
800
+ if (r.events?.length) eventsByResource[r.id] = r.events;
801
+ if (r.disabledBlocks?.length) blocksByResource[r.id] = r.disabledBlocks;
802
+ if (r.disableIntervals?.length) intervalsByResource[r.id] = r.disableIntervals;
803
+ }
804
+ setDayData({
805
+ events: eventsByResource,
806
+ disabledBlocks: blocksByResource,
807
+ disableIntervals: intervalsByResource
808
+ });
809
+ }, [resources, upsertResources, setDayData]);
810
+ return null;
811
+ };
812
+ var DisabledInterval = ({ width, top, height }) => {
813
+ return /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: [styles3.disabledBlock, { width, top, height }] }, /* @__PURE__ */ React18__namespace.default.createElement(Svg__default.default, { width, height: "100%" }, /* @__PURE__ */ React18__namespace.default.createElement(Svg.Defs, null, /* @__PURE__ */ React18__namespace.default.createElement(Svg.Pattern, { id: "diagonalHatch", patternUnits: "userSpaceOnUse", width: "10", height: "10" }, /* @__PURE__ */ React18__namespace.default.createElement(Svg.Line, { x1: "0", y1: "0", x2: "10", y2: "10", stroke: "rgba(150, 150, 150, 0.8)", strokeWidth: "1" }))), /* @__PURE__ */ React18__namespace.default.createElement(native.Rect, { width, height: "100%", fill: "url(#diagonalHatch)" })));
814
+ };
815
+ var DisabledIntervals = React18__namespace.default.memo(({ id, APPOINTMENT_BLOCK_WIDTH, hourHeight }) => {
816
+ const { useDisabledIntervalsFor: useDisabledIntervalsFor2 } = useCalendarBinding();
817
+ const disabledIntervals = useDisabledIntervalsFor2(id);
818
+ return /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, null, disabledIntervals.map(
819
+ (disabledInterval, index) => {
820
+ return /* @__PURE__ */ React18__namespace.default.createElement(
821
+ DisabledInterval,
822
+ {
823
+ key: `${index}-${disabledInterval.from}-${disabledInterval.to}`,
824
+ width: APPOINTMENT_BLOCK_WIDTH,
825
+ top: scalePosition(disabledInterval.from, hourHeight),
826
+ height: scalePosition(disabledInterval.to - disabledInterval.from, hourHeight)
827
+ }
828
+ );
829
+ }
830
+ ));
831
+ });
832
+ var styles3 = reactNative.StyleSheet.create({
833
+ disabledBlock: {
834
+ position: "absolute",
835
+ zIndex: -10
836
+ }
837
+ });
838
+ var DisabledIntervals_default = DisabledIntervals;
839
+ var Row = ({ children, divider, space, style, ...props }) => {
840
+ return /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: [{ flexDirection: "row" }, style], ...props }, React18__namespace.default.Children.toArray(children).map((child, index) => /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, { key: index }, child, index !== React18__namespace.default.Children.toArray(children).length - 1 && divider, index !== React18__namespace.default.Children.toArray(children).length - 1 && /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: { width: space, height: "100%" } }))));
841
+ };
842
+ var Row_default = Row;
843
+ var DisabledBlockComponent = ({
844
+ top,
845
+ height,
846
+ layout,
847
+ disabledBlock,
848
+ hourHeight,
849
+ onDisabledBlockPress
850
+ }) => {
851
+ const dynamicStyle = {
852
+ backgroundColor: "#d3d3d3",
853
+ top: top + 2,
854
+ height: height < hourHeight / 4 ? height : height - 4,
855
+ width: layout.widthPx,
856
+ left: layout.leftPx,
857
+ borderWidth: 1,
858
+ borderColor: "rgba(0,0,0,0.12)"
859
+ };
860
+ const titleFace = useResolvedFont({ fontWeight: "600" });
861
+ return /* @__PURE__ */ React18__namespace.default.createElement(
862
+ reactNative.TouchableOpacity,
863
+ {
864
+ style: [styles4.event, dynamicStyle],
865
+ onPress: () => {
866
+ onDisabledBlockPress && onDisabledBlockPress(disabledBlock);
867
+ }
868
+ },
869
+ /* @__PURE__ */ React18__namespace.default.createElement(Col_default, { style: { position: "relative" } }, /* @__PURE__ */ React18__namespace.default.createElement(Row_default, { style: { height: 18 } }, /* @__PURE__ */ React18__namespace.default.createElement(
870
+ reactNative.Text,
871
+ {
872
+ allowFontScaling: false,
873
+ style: {
874
+ fontFamily: titleFace,
875
+ fontSize: getTextSize(hourHeight)
876
+ }
877
+ },
878
+ minutesToTime(disabledBlock?.from),
879
+ " - ",
880
+ minutesToTime(disabledBlock?.to)
881
+ )), /* @__PURE__ */ React18__namespace.default.createElement(
882
+ reactNative.Text,
883
+ {
884
+ allowFontScaling: false,
885
+ style: {
886
+ fontFamily: titleFace,
887
+ fontSize: getTextSize(hourHeight)
888
+ }
889
+ },
890
+ disabledBlock?.title
891
+ ))
892
+ );
893
+ };
894
+ var DisabledBlocks = React18__namespace.default.memo(({
895
+ id,
896
+ APPOINTMENT_BLOCK_WIDTH,
897
+ hourHeight,
898
+ onDisabledBlockPress
899
+ }) => {
900
+ const { useDisabledBlocksFor: useDisabledBlocksFor2 } = useCalendarBinding();
901
+ const disabledBlocks = useDisabledBlocksFor2(id);
902
+ const layoutMap = React18.useMemo(() => {
903
+ return columnsToPixels(computeDisabledBlockColumns(disabledBlocks), APPOINTMENT_BLOCK_WIDTH);
904
+ }, [disabledBlocks]);
905
+ return /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, null, disabledBlocks.map(
906
+ (disabledBlock, index) => {
907
+ const key = disabledBlock.id;
908
+ return /* @__PURE__ */ React18__namespace.default.createElement(
909
+ DisabledBlockComponent,
910
+ {
911
+ hourHeight,
912
+ disabledBlock,
913
+ key: `${index}-${disabledBlock.from}-${disabledBlock.to}`,
914
+ top: scalePosition(disabledBlock.from, hourHeight),
915
+ height: scalePosition(disabledBlock.to - disabledBlock.from, hourHeight),
916
+ layout: layoutMap.get(key),
917
+ onDisabledBlockPress
918
+ }
919
+ );
920
+ }
921
+ ));
922
+ });
923
+ var styles4 = reactNative.StyleSheet.create({
924
+ event: {
925
+ position: "absolute",
926
+ borderRadius: 5,
927
+ padding: 2,
928
+ overflow: "hidden",
929
+ zIndex: 999
930
+ // Ensure events stay above the background blocks
931
+ }
932
+ });
933
+ var DisabledBlocks_default = DisabledBlocks;
934
+ var EventBlock = React18__namespace.default.memo(({
935
+ event,
936
+ onLongPress,
937
+ onPress,
938
+ disabled,
939
+ selected,
940
+ hourHeight,
941
+ slots,
942
+ frame,
943
+ styleOverrides
944
+ }) => {
945
+ const { useGetSelectedEvent: useGetSelectedEvent2 } = useCalendarBinding();
946
+ const selectedAppointment = useGetSelectedEvent2();
947
+ const eventTop = scalePosition(event.from, hourHeight);
948
+ const eventHeight = scalePosition(event.to - event.from, hourHeight);
949
+ const start = minutesToTime(event.from);
950
+ const end = minutesToTime(event.to);
951
+ const dynamicStyle = {
952
+ top: eventTop + 2,
953
+ height: eventHeight < hourHeight / 4 ? eventHeight : eventHeight - 4,
954
+ left: frame.leftPx,
955
+ width: frame.widthPx,
956
+ zIndex: frame.zIndex,
957
+ opacity: selectedAppointment ? 0.5 : 1,
958
+ borderWidth: selected ? 2 : 1,
959
+ borderColor: selected ? "#4d959c" : "rgba(0,0,0,0.12)"
960
+ };
961
+ const resolved = typeof styleOverrides === "function" ? styleOverrides(event) ?? {} : styleOverrides ?? {};
962
+ if (eventHeight == 0)
963
+ return null;
964
+ const TopRight = slots?.TopRight;
965
+ const Body = slots?.Body;
966
+ const titleFace = useResolvedFont({ fontWeight: "700" });
967
+ const timeFace = useResolvedFont({ fontWeight: "600" });
968
+ return /* @__PURE__ */ React18__namespace.default.createElement(
969
+ reactNative.TouchableOpacity,
970
+ {
971
+ style: [styles5.event, resolved?.container, dynamicStyle],
972
+ disabled,
973
+ onPress: () => {
974
+ onPress && onPress(event);
975
+ },
976
+ onLongPress: () => {
977
+ onLongPress && onLongPress(event);
978
+ }
979
+ },
980
+ /* @__PURE__ */ React18__namespace.default.createElement(Hidden_default, { isHidden: !disabled }, /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: {
981
+ position: "absolute",
982
+ top: 0,
983
+ width: "150%",
984
+ height: "150%",
985
+ zIndex: 1,
986
+ backgroundColor: "rgba(255, 255, 255, 0.5)"
987
+ } })),
988
+ /* @__PURE__ */ React18__namespace.default.createElement(Col_default, { style: [{ position: "relative" }, resolved?.content] }, /* @__PURE__ */ React18__namespace.default.createElement(
989
+ reactNative.TextInput,
990
+ {
991
+ editable: false,
992
+ allowFontScaling: false,
993
+ underlineColorAndroid: "transparent",
994
+ style: {
995
+ width: "100%",
996
+ fontFamily: timeFace,
997
+ fontSize: getTextSize(hourHeight),
998
+ pointerEvents: "none",
999
+ padding: 0,
1000
+ margin: 0
1001
+ },
1002
+ defaultValue: `${start} - ${end}`
1003
+ }
1004
+ ), Body ? /* @__PURE__ */ React18__namespace.default.createElement(Body, { event, ctx: { hourHeight } }) : /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, null, /* @__PURE__ */ React18__namespace.default.createElement(Row_default, { style: { alignItems: "center", height: 18 } }, /* @__PURE__ */ React18__namespace.default.createElement(
1005
+ reactNative.Text,
1006
+ {
1007
+ allowFontScaling: false,
1008
+ style: [{
1009
+ fontFamily: titleFace,
1010
+ fontSize: getTextSize(hourHeight),
1011
+ fontWeight: "700"
1012
+ }, resolved?.title]
1013
+ },
1014
+ event?.title
1015
+ )), /* @__PURE__ */ React18__namespace.default.createElement(
1016
+ reactNative.Text,
1017
+ {
1018
+ allowFontScaling: false,
1019
+ style: [{
1020
+ fontFamily: timeFace,
1021
+ fontSize: getTextSize(hourHeight),
1022
+ fontWeight: "600"
1023
+ }, resolved?.desc]
1024
+ },
1025
+ event?.description
1026
+ )), /* @__PURE__ */ React18__namespace.default.createElement(Row_default, { style: {
1027
+ position: "absolute",
1028
+ right: 2
1029
+ }, space: 2 }, TopRight ? /* @__PURE__ */ React18__namespace.default.createElement(TopRight, { event, ctx: { hourHeight } }) : null))
1030
+ );
1031
+ });
1032
+ var styles5 = reactNative.StyleSheet.create({
1033
+ event: {
1034
+ backgroundColor: "#4d959c",
1035
+ position: "absolute",
1036
+ borderRadius: 5,
1037
+ padding: 2,
1038
+ overflow: "hidden",
1039
+ zIndex: 9999
1040
+ // Ensure events stay above the background blocks
1041
+ }
1042
+ });
1043
+ var EventBlock_default = EventBlock;
1044
+ var AnimatedTextInput = Animated2__default.default.createAnimatedComponent(reactNative.TextInput);
1045
+ var DraggableEvent = ({
1046
+ selectedEvent,
1047
+ eventStartedTop,
1048
+ panYAbs,
1049
+ panXAbs,
1050
+ APPOINTMENT_BLOCK_WIDTH,
1051
+ hourHeight,
1052
+ eventHeight,
1053
+ styleOverrides,
1054
+ slots
1055
+ }) => {
1056
+ const dynamicStyle = Animated2.useAnimatedStyle(() => {
1057
+ return {
1058
+ height: eventHeight.value < hourHeight / 4 ? eventHeight.value : eventHeight.value - 4,
1059
+ width: APPOINTMENT_BLOCK_WIDTH,
1060
+ borderWidth: 1,
1061
+ borderColor: "rgba(0,0,0,0.12)"
1062
+ };
1063
+ });
1064
+ const draggingAnimatedStyle = Animated2.useAnimatedStyle(() => {
1065
+ if (!selectedEvent) {
1066
+ return {
1067
+ opacity: 0,
1068
+ transform: [
1069
+ {
1070
+ translateY: 0
1071
+ },
1072
+ {
1073
+ translateX: 0
1074
+ }
1075
+ ]
1076
+ };
1077
+ }
1078
+ return {
1079
+ opacity: 1,
1080
+ transform: [
1081
+ {
1082
+ translateY: panYAbs.value - eventHeight.value / 2 + 2
1083
+ },
1084
+ {
1085
+ translateX: panXAbs.value - APPOINTMENT_BLOCK_WIDTH / 2
1086
+ }
1087
+ ]
1088
+ };
1089
+ }, [selectedEvent, eventHeight, APPOINTMENT_BLOCK_WIDTH]);
1090
+ const initialDisplayTime = React18.useMemo(() => {
1091
+ const start = minutesToTime(positionToMinutes(eventStartedTop.value, hourHeight));
1092
+ const end = minutesToTime(positionToMinutes(eventStartedTop.value + eventHeight.value, hourHeight));
1093
+ return `${start} - ${end}`;
1094
+ }, [eventHeight.value, hourHeight, eventStartedTop.value]);
1095
+ const animatedTimeProps = Animated2.useAnimatedProps(() => {
1096
+ const start = minutesToTime(positionToMinutes(eventStartedTop.value, hourHeight));
1097
+ const end = minutesToTime(positionToMinutes(eventStartedTop.value + eventHeight.value, hourHeight));
1098
+ return {
1099
+ text: `${start} - ${end}`
1100
+ };
1101
+ }, []);
1102
+ const resolved = typeof styleOverrides === "function" ? styleOverrides(selectedEvent) ?? {} : styleOverrides ?? {};
1103
+ const TopRight = slots?.TopRight;
1104
+ const Body = slots?.Body;
1105
+ const titleFace = useResolvedFont({ fontWeight: "700" });
1106
+ const timeFace = useResolvedFont({ fontWeight: "600" });
1107
+ return /* @__PURE__ */ React18__namespace.createElement(Animated2__default.default.View, { style: [styles6.event, dynamicStyle, draggingAnimatedStyle, resolved?.container] }, /* @__PURE__ */ React18__namespace.createElement(Col_default, { style: [{ position: "relative" }, resolved?.content] }, /* @__PURE__ */ React18__namespace.createElement(
1108
+ AnimatedTextInput,
1109
+ {
1110
+ editable: false,
1111
+ allowFontScaling: false,
1112
+ underlineColorAndroid: "transparent",
1113
+ style: {
1114
+ width: "100%",
1115
+ fontFamily: timeFace,
1116
+ fontSize: getTextSize(hourHeight),
1117
+ pointerEvents: "none",
1118
+ padding: 0,
1119
+ margin: 0
1120
+ },
1121
+ defaultValue: initialDisplayTime,
1122
+ animatedProps: animatedTimeProps
1123
+ }
1124
+ ), Body ? /* @__PURE__ */ React18__namespace.createElement(Body, { event: selectedEvent, ctx: { hourHeight } }) : /* @__PURE__ */ React18__namespace.createElement(React18__namespace.Fragment, null, /* @__PURE__ */ React18__namespace.createElement(Row_default, { style: { alignItems: "center", height: 18 } }, /* @__PURE__ */ React18__namespace.createElement(
1125
+ reactNative.Text,
1126
+ {
1127
+ allowFontScaling: false,
1128
+ style: [{
1129
+ fontFamily: titleFace,
1130
+ fontSize: getTextSize(hourHeight)
1131
+ }, resolved?.title]
1132
+ },
1133
+ selectedEvent?.title
1134
+ )), /* @__PURE__ */ React18__namespace.createElement(
1135
+ reactNative.Text,
1136
+ {
1137
+ allowFontScaling: false,
1138
+ style: [{
1139
+ fontFamily: timeFace,
1140
+ fontSize: getTextSize(hourHeight)
1141
+ }, resolved?.desc]
1142
+ },
1143
+ selectedEvent?.description
1144
+ )), /* @__PURE__ */ React18__namespace.createElement(Row_default, { style: {
1145
+ position: "absolute",
1146
+ right: 2
1147
+ }, space: 2 }, TopRight ? /* @__PURE__ */ React18__namespace.createElement(TopRight, { event: selectedEvent, ctx: { hourHeight } }) : null)), /* @__PURE__ */ React18__namespace.createElement(Row_default, { style: {
1148
+ position: "absolute",
1149
+ alignSelf: "center",
1150
+ bottom: 0
1151
+ } }, /* @__PURE__ */ React18__namespace.createElement(vectorIcons.MaterialIcons, { name: "drag-handle", size: 12, color: "black" })));
1152
+ };
1153
+ var styles6 = reactNative.StyleSheet.create({
1154
+ event: {
1155
+ backgroundColor: "#4d959c",
1156
+ position: "absolute",
1157
+ borderRadius: 5,
1158
+ padding: 2,
1159
+ overflow: "hidden",
1160
+ zIndex: 99
1161
+ // Ensure events stay above the background blocks
1162
+ }
1163
+ });
1164
+ var EventBlocks = React18__namespace.default.memo(({
1165
+ id,
1166
+ onLongPress,
1167
+ onPress,
1168
+ hourHeight,
1169
+ EVENT_BLOCK_WIDTH,
1170
+ eventRenderer,
1171
+ isEventDisabled,
1172
+ isEventSelected,
1173
+ mode
1174
+ }) => {
1175
+ const { useEventsFor: useEventsFor2 } = useCalendarBinding();
1176
+ const events = useEventsFor2(id);
1177
+ const frameMap = React18.useMemo(
1178
+ () => computeEventFrames(events, EVENT_BLOCK_WIDTH, mode),
1179
+ [events, mode, EVENT_BLOCK_WIDTH]
1180
+ );
1181
+ const Renderer = eventRenderer;
1182
+ return events?.map(
1183
+ (evt, index) => {
1184
+ const selected = isEventSelected?.(evt) ?? false;
1185
+ const disabled = isEventDisabled?.(evt) ?? false;
1186
+ return /* @__PURE__ */ React18__namespace.default.createElement(
1187
+ Renderer,
1188
+ {
1189
+ key: `${evt.from}-${evt.to}-${index}`,
1190
+ event: evt,
1191
+ onLongPress: (evt2) => onLongPress(evt2),
1192
+ onPress: (evt2) => onPress(evt2),
1193
+ hourHeight,
1194
+ frame: frameMap.get(evt.id),
1195
+ selected,
1196
+ disabled
1197
+ }
1198
+ );
1199
+ }
1200
+ );
1201
+ });
1202
+ var EventBlocks_default = EventBlocks;
1203
+
1204
+ // src/components/Calendar.tsx
1205
+ var AnimatedFlashList = Animated2__default.default.createAnimatedComponent(flashList.FlashList);
1206
+ var CalendarInner = (props) => {
1207
+ const { width } = reactNative.useWindowDimensions();
1208
+ const isIOS = reactNative.Platform.OS === "ios";
1209
+ const binding = useCalendarBinding();
1210
+ const {
1211
+ date,
1212
+ numberOfColumns = 3,
1213
+ startMinutes,
1214
+ hourHeight = 120,
1215
+ snapIntervalInMinutes = 5,
1216
+ timezone = Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,
1217
+ resources,
1218
+ onResourcePress,
1219
+ onBlockLongPress,
1220
+ onEventPress,
1221
+ onEventLongPress,
1222
+ onDisabledBlockPress,
1223
+ eventSlots,
1224
+ eventStyleOverrides,
1225
+ overLappingLayoutMode = "stacked"
1226
+ } = props;
1227
+ const snapInterval = hourHeight / 60 * snapIntervalInMinutes;
1228
+ const onPressRef = React18__namespace.default.useRef(onEventPress);
1229
+ const onLongPressRef = React18__namespace.default.useRef(onEventLongPress);
1230
+ const internalOnLongPress = React18.useRef(null);
1231
+ const onDisabledBlockPressRef = React18__namespace.default.useRef(onDisabledBlockPress);
1232
+ const selectedRef = React18.useRef(props.isEventSelected);
1233
+ const disabledRef = React18.useRef(props.isEventDisabled);
1234
+ const effectiveRenderer = React18.useMemo(() => {
1235
+ return (p) => /* @__PURE__ */ React18__namespace.default.createElement(
1236
+ EventBlock_default,
1237
+ {
1238
+ ...p,
1239
+ slots: props.eventSlots,
1240
+ styleOverrides: props.eventStyleOverrides
1241
+ }
1242
+ );
1243
+ }, [eventSlots, eventStyleOverrides]);
1244
+ const isEventSelectedStable = React18.useCallback(
1245
+ (ev) => selectedRef.current ? selectedRef.current(ev) : false,
1246
+ []
1247
+ );
1248
+ const isEventDisabledStable = React18.useCallback(
1249
+ (ev) => disabledRef.current ? disabledRef.current(ev) : false,
1250
+ []
1251
+ );
1252
+ React18.useEffect(() => {
1253
+ onPressRef.current = onEventPress;
1254
+ }, [onEventPress]);
1255
+ React18.useEffect(() => {
1256
+ onLongPressRef.current = onEventLongPress;
1257
+ }, [onEventLongPress]);
1258
+ React18.useEffect(() => {
1259
+ onDisabledBlockPressRef.current = onDisabledBlockPress;
1260
+ }, [onDisabledBlockPress]);
1261
+ React18.useEffect(() => {
1262
+ rendererRef.current = effectiveRenderer;
1263
+ }, [effectiveRenderer]);
1264
+ React18.useEffect(() => {
1265
+ selectedRef.current = props.isEventSelected;
1266
+ }, [props.isEventSelected]);
1267
+ React18.useEffect(() => {
1268
+ disabledRef.current = props.isEventDisabled;
1269
+ }, [props.isEventDisabled]);
1270
+ const rendererRef = React18.useRef(effectiveRenderer);
1271
+ const stableRenderer = React18.useCallback((p) => rendererRef.current(p), []);
1272
+ const stableOnPress = React18__namespace.default.useCallback((e) => onPressRef.current?.(e), []);
1273
+ const stableOnDisabledBlockPress = React18__namespace.default.useCallback((b) => onDisabledBlockPressRef.current?.(b), []);
1274
+ const { useGetSelectedEvent: useGetSelectedEvent2, useSetSelectedEvent: useSetSelectedEvent2, useSetDraggedEventDraft: useSetDraggedEventDraft2, useGetDraggedEventDraft: useGetDraggedEventDraft2 } = useCalendarBinding();
1275
+ const selectedEvent = useGetSelectedEvent2();
1276
+ const setSelectedEvent = useSetSelectedEvent2();
1277
+ const setDraggedEventDraft = useSetDraggedEventDraft2();
1278
+ const APPOINTMENT_BLOCK_WIDTH = (width - TIME_LABEL_WIDTH) / numberOfColumns;
1279
+ const hourHeightRef = React18.useRef(hourHeight);
1280
+ const resourcesRef = React18.useRef(resources);
1281
+ const apptWidthRef = React18.useRef(APPOINTMENT_BLOCK_WIDTH);
1282
+ React18.useEffect(() => {
1283
+ hourHeightRef.current = hourHeight;
1284
+ }, [hourHeight]);
1285
+ React18.useEffect(() => {
1286
+ resourcesRef.current = resources;
1287
+ }, [resources]);
1288
+ React18.useEffect(() => {
1289
+ apptWidthRef.current = APPOINTMENT_BLOCK_WIDTH;
1290
+ }, [APPOINTMENT_BLOCK_WIDTH]);
1291
+ React18.useEffect(() => {
1292
+ if (!selectedEvent) {
1293
+ setDraggedEventDraft(null);
1294
+ }
1295
+ }, [selectedEvent]);
1296
+ const verticalScrollViewRef = Animated2.useAnimatedRef();
1297
+ const headerScrollViewRef = Animated2.useAnimatedRef();
1298
+ const flashListRef = React18.useRef(null);
1299
+ const prevResourceIdsRef = React18.useRef([]);
1300
+ const [layout, setLayout] = React18.useState(null);
1301
+ const dateRef = React18.useRef(date);
1302
+ const eventStartedTop = Animated2.useSharedValue(0);
1303
+ const eventHeight = Animated2.useSharedValue(0);
1304
+ const panXAbs = Animated2.useSharedValue(0);
1305
+ const panYAbs = Animated2.useSharedValue(0);
1306
+ const isPulling = Animated2.useSharedValue(false);
1307
+ const isDragging = Animated2.useSharedValue(false);
1308
+ const scrollX = Animated2.useSharedValue(0);
1309
+ const scrollY = Animated2.useSharedValue(0);
1310
+ const autoScrollSpeed = Animated2.useSharedValue(0);
1311
+ const autoScrollXSpeed = Animated2.useSharedValue(0);
1312
+ const lastHapticScrollY = Animated2.useSharedValue(0);
1313
+ const lastXScrollTime = Animated2.useSharedValue(0);
1314
+ const startedX = Animated2.useSharedValue(0);
1315
+ const startedY = Animated2.useSharedValue(0);
1316
+ const touchY = Animated2.useSharedValue(0);
1317
+ const triggerHaptic = React18.useCallback(() => {
1318
+ Haptics__namespace.impactAsync(Haptics__namespace.ImpactFeedbackStyle.Light);
1319
+ }, []);
1320
+ const panGesture = reactNativeGestureHandler.Gesture.Pan().manualActivation(!isIOS).enabled(layout !== null).shouldCancelWhenOutside(false).onTouchesMove((_evt, stateManager) => {
1321
+ if (isIOS) return;
1322
+ if (selectedEvent)
1323
+ stateManager.activate();
1324
+ else stateManager.end();
1325
+ }).onUpdate((evt) => {
1326
+ if (!evt || evt.y == null || evt.x == null) return;
1327
+ let draggable = false;
1328
+ let pullable = false;
1329
+ const draggableMinY = panYAbs.value - eventHeight.value / 2;
1330
+ const draggableMaxY = panYAbs.value + eventHeight.value / 2 - (eventHeight.value <= snapInterval * 3 * 2 ? snapInterval : snapInterval * 3);
1331
+ const pullableMaxY = panYAbs.value + eventHeight.value / 2;
1332
+ const blockMinX = panXAbs.value - APPOINTMENT_BLOCK_WIDTH / 2;
1333
+ const blockMaxX = panXAbs.value + APPOINTMENT_BLOCK_WIDTH / 2;
1334
+ touchY.value = evt.y;
1335
+ if (evt.x >= blockMinX && evt.x <= blockMaxX) {
1336
+ draggable = evt.y >= draggableMinY && evt.y <= draggableMaxY;
1337
+ pullable = evt.y > draggableMaxY && evt.y <= pullableMaxY + snapInterval * 3;
1338
+ }
1339
+ if (pullable && !isDragging.value || isPulling.value) {
1340
+ isPulling.value = true;
1341
+ const onScreenTop = eventStartedTop.value - scrollY.value;
1342
+ const newHeight = evt.y - onScreenTop;
1343
+ const snappedHeight = Math.round(newHeight / snapInterval) * snapInterval;
1344
+ let finalHeight = Math.max(hourHeight / 4, snappedHeight);
1345
+ const totalDayHeight = 24 * hourHeight;
1346
+ const maxAllowedHeight = totalDayHeight - eventStartedTop.value;
1347
+ finalHeight = Math.min(finalHeight, maxAllowedHeight);
1348
+ if (finalHeight !== eventHeight.value) {
1349
+ eventHeight.value = finalHeight;
1350
+ panYAbs.value = onScreenTop + finalHeight / 2;
1351
+ Animated2.runOnJS(triggerHaptic)();
1352
+ }
1353
+ if (layout) {
1354
+ const AUTO_SCROLL_BUFFER = 30;
1355
+ if (evt.y > layout.height - AUTO_SCROLL_BUFFER) {
1356
+ autoScrollSpeed.value = 1;
1357
+ } else if (evt.y < AUTO_SCROLL_BUFFER && newHeight > hourHeight / 4) {
1358
+ autoScrollSpeed.value = -1;
1359
+ } else {
1360
+ autoScrollSpeed.value = 0;
1361
+ }
1362
+ } else {
1363
+ autoScrollSpeed.value = 0;
1364
+ }
1365
+ }
1366
+ if (draggable && !isPulling.value || isDragging.value) {
1367
+ isDragging.value = true;
1368
+ const translatedY = Math.round(evt.translationY / snapInterval) * snapInterval;
1369
+ const proposedAbsoluteTop = startedY.value - eventHeight.value / 2 + translatedY + scrollY.value;
1370
+ let snappedAbsoluteTop = Math.round(proposedAbsoluteTop / snapInterval) * snapInterval;
1371
+ snappedAbsoluteTop = Math.max(0, snappedAbsoluteTop);
1372
+ if (layout) {
1373
+ const maxAbsoluteTop = layout.height + scrollY.value - snapInterval;
1374
+ snappedAbsoluteTop = Math.min(snappedAbsoluteTop, maxAbsoluteTop);
1375
+ }
1376
+ if (snappedAbsoluteTop !== eventStartedTop.value) {
1377
+ Animated2.runOnJS(triggerHaptic)();
1378
+ eventStartedTop.value = snappedAbsoluteTop;
1379
+ }
1380
+ panYAbs.value = snappedAbsoluteTop - scrollY.value + eventHeight.value / 2;
1381
+ let panXAbsValue = Math.max(
1382
+ APPOINTMENT_BLOCK_WIDTH / 2 + TIME_LABEL_WIDTH,
1383
+ startedX.value + evt.translationX
1384
+ );
1385
+ if (layout?.width) {
1386
+ panXAbsValue = Math.min(
1387
+ layout.width - APPOINTMENT_BLOCK_WIDTH / 2,
1388
+ panXAbsValue
1389
+ );
1390
+ }
1391
+ panXAbs.value = panXAbsValue;
1392
+ if (layout) {
1393
+ const AUTO_SCROLL_BUFFER = 30;
1394
+ if (evt.y > layout.height - AUTO_SCROLL_BUFFER) {
1395
+ autoScrollSpeed.value = 1;
1396
+ } else if (evt.y < AUTO_SCROLL_BUFFER) {
1397
+ autoScrollSpeed.value = -1;
1398
+ } else {
1399
+ autoScrollSpeed.value = 0;
1400
+ }
1401
+ if (panXAbs.value >= layout.width - APPOINTMENT_BLOCK_WIDTH / 2) {
1402
+ autoScrollXSpeed.value = 1;
1403
+ } else if (panXAbs.value <= APPOINTMENT_BLOCK_WIDTH / 2 + TIME_LABEL_WIDTH) {
1404
+ autoScrollXSpeed.value = -1;
1405
+ } else {
1406
+ autoScrollXSpeed.value = 0;
1407
+ }
1408
+ } else {
1409
+ autoScrollSpeed.value = 0;
1410
+ autoScrollXSpeed.value = 0;
1411
+ }
1412
+ }
1413
+ }).onEnd(() => {
1414
+ autoScrollSpeed.value = 0;
1415
+ autoScrollXSpeed.value = 0;
1416
+ lastXScrollTime.value = 0;
1417
+ const finalEventTop = panYAbs.value - eventHeight.value / 2 + scrollY.value;
1418
+ let adjustedFinalEventTop = Math.round(finalEventTop / snapInterval) * snapInterval;
1419
+ adjustedFinalEventTop = Math.max(0, adjustedFinalEventTop);
1420
+ const finalPanYValue = adjustedFinalEventTop - scrollY.value + eventHeight.value / 2;
1421
+ const finalXOnScreen = panXAbs.value;
1422
+ const absoluteX = finalXOnScreen + scrollX.value;
1423
+ const newStaffIndex = Math.floor((absoluteX - TIME_LABEL_WIDTH) / APPOINTMENT_BLOCK_WIDTH);
1424
+ const clampedStaffIndex = Math.max(0, Math.min(newStaffIndex, resources.length - 1));
1425
+ const endedResource = resources[clampedStaffIndex];
1426
+ const finalPanXValue = TIME_LABEL_WIDTH + clampedStaffIndex * APPOINTMENT_BLOCK_WIDTH - scrollX.value + APPOINTMENT_BLOCK_WIDTH / 2;
1427
+ panYAbs.value = Animated2.withSpring(finalPanYValue);
1428
+ panXAbs.value = Animated2.withSpring(finalPanXValue);
1429
+ if (!isPulling.value) {
1430
+ eventStartedTop.value = adjustedFinalEventTop;
1431
+ }
1432
+ startedY.value = finalPanYValue;
1433
+ startedX.value = finalPanXValue;
1434
+ isPulling.value = false;
1435
+ isDragging.value = false;
1436
+ Animated2.runOnJS(setDraggedEventDraft)({
1437
+ event: selectedEvent,
1438
+ from: positionToMinutes(adjustedFinalEventTop, hourHeight),
1439
+ to: positionToMinutes(adjustedFinalEventTop + eventHeight.value, hourHeight),
1440
+ resourceId: endedResource?.id
1441
+ });
1442
+ });
1443
+ const scrollListTo = (x) => {
1444
+ flashListRef.current?.scrollToOffset({ offset: x, animated: false });
1445
+ };
1446
+ Animated2.useFrameCallback((frameInfo) => {
1447
+ if (autoScrollXSpeed.value === 0) {
1448
+ return;
1449
+ }
1450
+ const now = frameInfo.timeSinceFirstFrame;
1451
+ const scrollInterval = 500;
1452
+ if (now - lastXScrollTime.value > scrollInterval) {
1453
+ lastXScrollTime.value = now;
1454
+ const increment = APPOINTMENT_BLOCK_WIDTH * Math.sign(autoScrollXSpeed.value);
1455
+ const newScrollX = scrollX.value + increment;
1456
+ Animated2.runOnJS(scrollListTo)(newScrollX);
1457
+ Animated2.runOnJS(Haptics__namespace.impactAsync)(Haptics__namespace.ImpactFeedbackStyle.Medium);
1458
+ }
1459
+ });
1460
+ Animated2.useFrameCallback(() => {
1461
+ if (autoScrollSpeed.value === 0) {
1462
+ return;
1463
+ }
1464
+ const increment = snapInterval / 5 * Math.sign(autoScrollSpeed.value);
1465
+ const newScrollY = scrollY.value + increment;
1466
+ Animated2.scrollTo(verticalScrollViewRef, 0, newScrollY, false);
1467
+ if (isDragging.value) {
1468
+ let currentEventTop = panYAbs.value - eventHeight.value / 2 + newScrollY;
1469
+ currentEventTop = Math.round(currentEventTop / snapInterval) * snapInterval;
1470
+ eventStartedTop.value = Math.max(0, currentEventTop);
1471
+ }
1472
+ if (isPulling.value) {
1473
+ const onScreenTop = eventStartedTop.value - newScrollY;
1474
+ const newHeight = touchY.value - onScreenTop;
1475
+ const snappedHeight = Math.round(newHeight / snapInterval) * snapInterval;
1476
+ let finalHeight = Math.max(hourHeight / 4, snappedHeight);
1477
+ const totalDayHeight = 24 * hourHeight;
1478
+ const maxAllowedHeight = totalDayHeight - eventStartedTop.value;
1479
+ finalHeight = Math.min(finalHeight, maxAllowedHeight);
1480
+ if (finalHeight !== eventHeight.value) {
1481
+ eventHeight.value = finalHeight;
1482
+ panYAbs.value = onScreenTop + finalHeight / 2;
1483
+ }
1484
+ if (hourHeight / 4 == finalHeight) {
1485
+ autoScrollSpeed.value = 0;
1486
+ }
1487
+ }
1488
+ const scrollDiff = Math.abs(newScrollY - lastHapticScrollY.value);
1489
+ if (scrollDiff >= snapInterval) {
1490
+ lastHapticScrollY.value = newScrollY;
1491
+ Animated2.runOnJS(Haptics__namespace.impactAsync)(Haptics__namespace.ImpactFeedbackStyle.Medium);
1492
+ }
1493
+ });
1494
+ React18.useEffect(() => {
1495
+ internalOnLongPress.current = (event) => {
1496
+ onLongPressRef.current?.(event);
1497
+ const hh = hourHeightRef.current;
1498
+ const eventTop = scalePosition(event.from, hh);
1499
+ const eventTo = event.to < event.from ? event.to + 1440 : event.to;
1500
+ const initialHeight = scalePosition(eventTo - event.from, hh);
1501
+ const panAbsValue = eventTop - scrollY.value + initialHeight / 2;
1502
+ panYAbs.value = panAbsValue;
1503
+ startedY.value = panAbsValue;
1504
+ eventStartedTop.value = eventTop;
1505
+ const resources2 = resourcesRef.current;
1506
+ const APPOINTMENT_BLOCK_WIDTH2 = apptWidthRef.current;
1507
+ const staffIndex = resources2.findIndex((r) => r.id === event.resourceId);
1508
+ const leftmostColumnIndex = Math.round(scrollX.value / APPOINTMENT_BLOCK_WIDTH2);
1509
+ const screenColumn = staffIndex - leftmostColumnIndex;
1510
+ const selectedAppointmentStartedX = TIME_LABEL_WIDTH + APPOINTMENT_BLOCK_WIDTH2 / 2 + APPOINTMENT_BLOCK_WIDTH2 * screenColumn;
1511
+ panXAbs.value = selectedAppointmentStartedX;
1512
+ startedX.value = selectedAppointmentStartedX;
1513
+ lastHapticScrollY.value = scrollY.value;
1514
+ eventHeight.value = initialHeight;
1515
+ setSelectedEvent(event);
1516
+ Haptics__namespace.impactAsync(Haptics__namespace.ImpactFeedbackStyle.Medium);
1517
+ };
1518
+ }, []);
1519
+ const internalStableOnLongPress = React18.useCallback((e) => {
1520
+ internalOnLongPress.current?.(e);
1521
+ }, []);
1522
+ const onLayout = React18.useCallback((evt) => {
1523
+ setLayout(evt?.nativeEvent?.layout);
1524
+ }, []);
1525
+ const verticalScrollHandler = Animated2.useAnimatedScrollHandler({
1526
+ onScroll: (event) => {
1527
+ scrollY.value = event?.contentOffset?.y;
1528
+ }
1529
+ });
1530
+ const flashListScrollHandler = Animated2.useAnimatedScrollHandler({
1531
+ onScroll: (event) => {
1532
+ const offsetX = event?.contentOffset?.x;
1533
+ Animated2.scrollTo(headerScrollViewRef, offsetX, 0, false);
1534
+ scrollX.value = offsetX;
1535
+ }
1536
+ });
1537
+ const resourceIds = React18.useMemo(() => {
1538
+ const ids = resources?.map((item) => item?.id) || [];
1539
+ if (JSON.stringify(prevResourceIdsRef.current) !== JSON.stringify(ids)) {
1540
+ prevResourceIdsRef.current = ids;
1541
+ }
1542
+ return prevResourceIdsRef.current;
1543
+ }, [resources]);
1544
+ const handleBlockPress = React18.useCallback((resourceId, time) => {
1545
+ Haptics__namespace.impactAsync(Haptics__namespace.ImpactFeedbackStyle.Medium);
1546
+ const resource = resources.find((r) => r.id === resourceId);
1547
+ if (onBlockLongPress)
1548
+ onBlockLongPress(resource, new Date(time));
1549
+ }, [resources, onBlockLongPress]);
1550
+ React18.useEffect(() => {
1551
+ const handleOrientationChange = () => {
1552
+ if (selectedEvent)
1553
+ setSelectedEvent(null);
1554
+ };
1555
+ const subscription = reactNative.Dimensions.addEventListener("change", handleOrientationChange);
1556
+ return () => {
1557
+ subscription.remove();
1558
+ };
1559
+ }, [setSelectedEvent, selectedEvent]);
1560
+ React18.useEffect(() => {
1561
+ dateRef.current = date;
1562
+ }, [date]);
1563
+ const renderItem = React18.useCallback(({ item }) => {
1564
+ return /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { key: item, style: { width: APPOINTMENT_BLOCK_WIDTH } }, /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: styles7.timelineContainer }, /* @__PURE__ */ React18__namespace.default.createElement(
1565
+ EventGridBlocksSkia,
1566
+ {
1567
+ dateRef,
1568
+ hourHeight,
1569
+ APPOINTMENT_BLOCK_WIDTH,
1570
+ handleBlockPress: (date2) => handleBlockPress(item, date2)
1571
+ }
1572
+ ), /* @__PURE__ */ React18__namespace.default.createElement(
1573
+ DisabledIntervals_default,
1574
+ {
1575
+ id: item,
1576
+ APPOINTMENT_BLOCK_WIDTH,
1577
+ hourHeight
1578
+ }
1579
+ ), /* @__PURE__ */ React18__namespace.default.createElement(
1580
+ DisabledBlocks_default,
1581
+ {
1582
+ id: item,
1583
+ APPOINTMENT_BLOCK_WIDTH,
1584
+ hourHeight,
1585
+ onDisabledBlockPress: stableOnDisabledBlockPress
1586
+ }
1587
+ ), /* @__PURE__ */ React18__namespace.default.createElement(
1588
+ EventBlocks_default,
1589
+ {
1590
+ id: item,
1591
+ EVENT_BLOCK_WIDTH: APPOINTMENT_BLOCK_WIDTH,
1592
+ hourHeight,
1593
+ onPress: stableOnPress,
1594
+ onLongPress: internalStableOnLongPress,
1595
+ isEventSelected: isEventSelectedStable,
1596
+ isEventDisabled: isEventDisabledStable,
1597
+ eventRenderer: stableRenderer,
1598
+ mode: overLappingLayoutMode
1599
+ }
1600
+ )));
1601
+ }, [
1602
+ resourceIds,
1603
+ APPOINTMENT_BLOCK_WIDTH,
1604
+ hourHeight,
1605
+ stableRenderer,
1606
+ isEventSelectedStable,
1607
+ isEventDisabledStable,
1608
+ overLappingLayoutMode,
1609
+ stableOnPress,
1610
+ internalStableOnLongPress,
1611
+ stableOnDisabledBlockPress,
1612
+ dateRef
1613
+ ]);
1614
+ return /* @__PURE__ */ React18__namespace.default.createElement(React18__namespace.default.Fragment, null, /* @__PURE__ */ React18__namespace.default.createElement(StoreFeeder, { resources, store: binding }), /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: { flex: 1 } }, /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, null, /* @__PURE__ */ React18__namespace.default.createElement(
1615
+ Animated2__default.default.ScrollView,
1616
+ {
1617
+ style: { backgroundColor: "white" },
1618
+ showsHorizontalScrollIndicator: false,
1619
+ contentContainerStyle: {
1620
+ overflow: "visible",
1621
+ paddingLeft: TIME_LABEL_WIDTH,
1622
+ paddingVertical: 15
1623
+ },
1624
+ horizontal: true,
1625
+ scrollEventThrottle: 16,
1626
+ decelerationRate: "fast",
1627
+ ref: headerScrollViewRef,
1628
+ scrollEnabled: false
1629
+ },
1630
+ /* @__PURE__ */ React18__namespace.default.createElement(
1631
+ ResourcesComponent,
1632
+ {
1633
+ resourceIds,
1634
+ APPOINTMENT_BLOCK_WIDTH,
1635
+ onResourcePress
1636
+ }
1637
+ )
1638
+ )), /* @__PURE__ */ React18__namespace.default.createElement(reactNativeGestureHandler.GestureDetector, { gesture: panGesture }, /* @__PURE__ */ React18__namespace.default.createElement(
1639
+ Animated2__default.default.View,
1640
+ {
1641
+ key: numberOfColumns + width + hourHeight,
1642
+ onLayout,
1643
+ style: {
1644
+ flex: 1,
1645
+ overflow: "hidden"
1646
+ }
1647
+ },
1648
+ selectedEvent && /* @__PURE__ */ React18__namespace.default.createElement(reactNative.View, { style: {
1649
+ position: "absolute",
1650
+ top: 0,
1651
+ left: TIME_LABEL_WIDTH,
1652
+ paddingLeft: TIME_LABEL_WIDTH,
1653
+ width: width - TIME_LABEL_WIDTH,
1654
+ height: "100%",
1655
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
1656
+ zIndex: 1
1657
+ } }),
1658
+ /* @__PURE__ */ React18__namespace.default.createElement(
1659
+ Animated2__default.default.ScrollView,
1660
+ {
1661
+ scrollEnabled: !selectedEvent,
1662
+ onScroll: verticalScrollHandler,
1663
+ ref: verticalScrollViewRef,
1664
+ scrollEventThrottle: 16,
1665
+ snapToInterval: hourHeight,
1666
+ decelerationRate: "fast",
1667
+ snapToAlignment: "start",
1668
+ style: styles7.container,
1669
+ contentContainerStyle: { flexDirection: "row", paddingRight: TIME_LABEL_WIDTH }
1670
+ },
1671
+ /* @__PURE__ */ React18__namespace.default.createElement(
1672
+ TimeLabels,
1673
+ {
1674
+ startMinutes,
1675
+ layout,
1676
+ hourHeight,
1677
+ totalTimelineWidth: APPOINTMENT_BLOCK_WIDTH * numberOfColumns,
1678
+ timezone,
1679
+ date,
1680
+ ref: verticalScrollViewRef
1681
+ }
1682
+ ),
1683
+ /* @__PURE__ */ React18__namespace.default.createElement(
1684
+ AnimatedFlashList,
1685
+ {
1686
+ extraData: numberOfColumns + width + hourHeight + (overLappingLayoutMode === "stacked" ? 1 : 0),
1687
+ scrollEnabled: !selectedEvent,
1688
+ ref: flashListRef,
1689
+ onScroll: flashListScrollHandler,
1690
+ estimatedItemSize: APPOINTMENT_BLOCK_WIDTH,
1691
+ removeClippedSubviews: true,
1692
+ data: resourceIds,
1693
+ horizontal: true,
1694
+ renderItem,
1695
+ keyExtractor: (item, index) => index + "",
1696
+ snapToInterval: APPOINTMENT_BLOCK_WIDTH,
1697
+ decelerationRate: "fast",
1698
+ snapToAlignment: "start"
1699
+ }
1700
+ )
1701
+ ),
1702
+ selectedEvent && /* @__PURE__ */ React18__namespace.default.createElement(
1703
+ DraggableEvent,
1704
+ {
1705
+ selectedEvent,
1706
+ APPOINTMENT_BLOCK_WIDTH,
1707
+ hourHeight,
1708
+ eventStartedTop,
1709
+ eventHeight,
1710
+ panXAbs,
1711
+ panYAbs,
1712
+ slots: props.eventSlots,
1713
+ styleOverrides: props.eventStyleOverrides
1714
+ }
1715
+ )
1716
+ ))));
1717
+ };
1718
+ var Calendar = ({ theme, ...rest }) => {
1719
+ return /* @__PURE__ */ React18__namespace.default.createElement(CalendarThemeProvider, { theme }, /* @__PURE__ */ React18__namespace.default.createElement(CalendarInner, { ...rest }));
1720
+ };
1721
+ var styles7 = reactNative.StyleSheet.create({
1722
+ container: {
1723
+ flex: 1,
1724
+ backgroundColor: "#fff"
1725
+ },
1726
+ timelineContainer: {
1727
+ borderColor: "#ddd",
1728
+ borderRightWidth: 1,
1729
+ position: "relative",
1730
+ height: "100%"
1731
+ }
1732
+ });
1733
+ var Calendar_default = Calendar;
1734
+
1735
+ exports.Calendar = Calendar_default;
1736
+ exports.CalendarBindingProvider = CalendarBindingProvider;
1737
+ exports.useCalendarBinding = useCalendarBinding;
1738
+ //# sourceMappingURL=index.js.map
1739
+ //# sourceMappingURL=index.js.map