@entro314labs/react-arc-tabs 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.cjs ADDED
@@ -0,0 +1,1028 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ArcTabs: () => ArcTabs,
34
+ ArcTabsTailwind: () => ArcTabsTailwind
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/ArcTabs.tsx
39
+ var React = __toESM(require("react"), 1);
40
+ var import_jsx_runtime = require("react/jsx-runtime");
41
+ var joinClassNames = (...parts) => parts.filter(Boolean).join(" ");
42
+ var toCssSize = (value) => typeof value === "number" ? `${value}px` : value;
43
+ var findFirstEnabledIndex = (items) => items.findIndex((item) => !item.disabled);
44
+ var getEnabledIndices = (items) => items.reduce((acc, item, index) => {
45
+ if (!item.disabled) acc.push(index);
46
+ return acc;
47
+ }, []);
48
+ var getNextEnabledIndex = (enabledIndices, currentIndex, direction) => {
49
+ if (!enabledIndices.length) return -1;
50
+ const currentPosition = enabledIndices.indexOf(currentIndex);
51
+ if (currentPosition === -1) {
52
+ return direction === 1 ? enabledIndices[0] ?? -1 : enabledIndices[enabledIndices.length - 1] ?? -1;
53
+ }
54
+ const nextPosition = (currentPosition + direction + enabledIndices.length) % enabledIndices.length;
55
+ return enabledIndices[nextPosition] ?? -1;
56
+ };
57
+ function ArcTabs({
58
+ items,
59
+ value,
60
+ defaultValue,
61
+ onValueChange,
62
+ activationMode = "automatic",
63
+ keepMounted = true,
64
+ size = "md",
65
+ fit = "content",
66
+ motionPreset = "subtle",
67
+ motionDuration = 260,
68
+ ariaLabel = "Tabs",
69
+ listId,
70
+ tabsClassName,
71
+ panelClassName,
72
+ radius,
73
+ gap,
74
+ panelPadding,
75
+ accentColor,
76
+ tabBackground,
77
+ tabHoverBackground,
78
+ panelBackground,
79
+ panelBorderColor,
80
+ emptyState = null,
81
+ renderTabLabel,
82
+ renderPanel,
83
+ className,
84
+ style,
85
+ ...rest
86
+ }) {
87
+ const reactId = React.useId();
88
+ const baseId = React.useMemo(
89
+ () => (listId ?? `arc-tabs-${reactId}`).replace(/:/g, ""),
90
+ [listId, reactId]
91
+ );
92
+ const isControlled = value !== void 0;
93
+ const firstEnabledIndex = React.useMemo(
94
+ () => findFirstEnabledIndex(items),
95
+ [items]
96
+ );
97
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(() => {
98
+ const requested = defaultValue;
99
+ const requestedMatch = items.find(
100
+ (item) => !item.disabled && item.id === requested
101
+ );
102
+ if (requestedMatch) return requestedMatch.id;
103
+ return firstEnabledIndex >= 0 ? items[firstEnabledIndex]?.id : void 0;
104
+ });
105
+ const rawValue = isControlled ? value : uncontrolledValue;
106
+ const strictSelectedIndex = React.useMemo(
107
+ () => items.findIndex((item) => !item.disabled && item.id === rawValue),
108
+ [items, rawValue]
109
+ );
110
+ const selectedIndex = strictSelectedIndex >= 0 ? strictSelectedIndex : firstEnabledIndex;
111
+ const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : void 0;
112
+ React.useEffect(() => {
113
+ if (isControlled) return;
114
+ if (strictSelectedIndex !== -1) return;
115
+ if (firstEnabledIndex !== -1) {
116
+ const fallbackId = items[firstEnabledIndex]?.id;
117
+ setUncontrolledValue(fallbackId);
118
+ } else {
119
+ setUncontrolledValue(void 0);
120
+ }
121
+ }, [isControlled, strictSelectedIndex, firstEnabledIndex, items]);
122
+ const [focusedIndex, setFocusedIndex] = React.useState(selectedIndex);
123
+ React.useEffect(() => {
124
+ if (selectedIndex === -1) {
125
+ setFocusedIndex(-1);
126
+ return;
127
+ }
128
+ if (focusedIndex < 0 || focusedIndex >= items.length || items[focusedIndex]?.disabled) {
129
+ setFocusedIndex(selectedIndex);
130
+ }
131
+ }, [focusedIndex, selectedIndex, items]);
132
+ const enabledIndices = React.useMemo(() => getEnabledIndices(items), [items]);
133
+ const tabRefs = React.useRef([]);
134
+ const listRef = React.useRef(null);
135
+ const activePanelRef = React.useRef(null);
136
+ const hasMountedRef = React.useRef(false);
137
+ const previousSelectedIndexRef = React.useRef(selectedIndex);
138
+ const [hasInteracted, setHasInteracted] = React.useState(false);
139
+ const [panelDirection, setPanelDirection] = React.useState("none");
140
+ const [indicator, setIndicator] = React.useState({
141
+ x: 0,
142
+ width: 0,
143
+ ready: false
144
+ });
145
+ const effectiveMotionDuration = motionPreset === "none" ? 0 : Math.max(0, motionDuration);
146
+ const showSlidingIndicator = motionPreset === "expressive" && selectedIndex >= 0;
147
+ React.useEffect(() => {
148
+ tabRefs.current = tabRefs.current.slice(0, items.length);
149
+ }, [items.length]);
150
+ React.useEffect(() => {
151
+ const previous = previousSelectedIndexRef.current;
152
+ if (!hasMountedRef.current) {
153
+ hasMountedRef.current = true;
154
+ previousSelectedIndexRef.current = selectedIndex;
155
+ return;
156
+ }
157
+ if (previous !== selectedIndex) {
158
+ setHasInteracted(true);
159
+ if (selectedIndex >= 0 && previous >= 0) {
160
+ setPanelDirection(selectedIndex > previous ? "forward" : "backward");
161
+ }
162
+ }
163
+ previousSelectedIndexRef.current = selectedIndex;
164
+ }, [selectedIndex]);
165
+ const focusTabIndex = React.useCallback((index) => {
166
+ if (index < 0) return;
167
+ setFocusedIndex(index);
168
+ tabRefs.current[index]?.focus();
169
+ }, []);
170
+ const selectTab = React.useCallback(
171
+ (index) => {
172
+ const item = items[index];
173
+ if (!item || item.disabled) return;
174
+ if (index === selectedIndex) {
175
+ setFocusedIndex(index);
176
+ return;
177
+ }
178
+ setHasInteracted(true);
179
+ if (selectedIndex >= 0) {
180
+ setPanelDirection(index > selectedIndex ? "forward" : "backward");
181
+ }
182
+ if (!isControlled) {
183
+ setUncontrolledValue(item.id);
184
+ }
185
+ setFocusedIndex(index);
186
+ onValueChange?.(item.id, item, index);
187
+ },
188
+ [items, selectedIndex, isControlled, onValueChange]
189
+ );
190
+ const syncIndicator = React.useCallback(() => {
191
+ if (!showSlidingIndicator) {
192
+ setIndicator(
193
+ (previous) => previous.ready || previous.width !== 0 || previous.x !== 0 ? { x: 0, width: 0, ready: false } : previous
194
+ );
195
+ return;
196
+ }
197
+ const listElement = listRef.current;
198
+ const selectedTab = selectedIndex >= 0 ? tabRefs.current[selectedIndex] ?? null : null;
199
+ if (!listElement || !selectedTab) return;
200
+ const listRect = listElement.getBoundingClientRect();
201
+ const tabRect = selectedTab.getBoundingClientRect();
202
+ const nextX = tabRect.left - listRect.left + listElement.scrollLeft;
203
+ const nextWidth = tabRect.width;
204
+ setIndicator((previous) => {
205
+ const changedX = Math.abs(previous.x - nextX) > 0.5;
206
+ const changedWidth = Math.abs(previous.width - nextWidth) > 0.5;
207
+ if (!changedX && !changedWidth && previous.ready) {
208
+ return previous;
209
+ }
210
+ return {
211
+ x: nextX,
212
+ width: nextWidth,
213
+ ready: true
214
+ };
215
+ });
216
+ }, [selectedIndex, showSlidingIndicator]);
217
+ React.useEffect(() => {
218
+ syncIndicator();
219
+ }, [syncIndicator, items.length, size, fit]);
220
+ React.useEffect(() => {
221
+ if (!showSlidingIndicator) return;
222
+ const listElement = listRef.current;
223
+ if (!listElement) return;
224
+ const onResize = () => syncIndicator();
225
+ const onScroll = () => syncIndicator();
226
+ const frame = requestAnimationFrame(syncIndicator);
227
+ let observer = null;
228
+ if (typeof ResizeObserver !== "undefined") {
229
+ observer = new ResizeObserver(() => syncIndicator());
230
+ observer.observe(listElement);
231
+ tabRefs.current.forEach((tabElement) => {
232
+ if (tabElement) observer?.observe(tabElement);
233
+ });
234
+ }
235
+ listElement.addEventListener("scroll", onScroll, { passive: true });
236
+ window.addEventListener("resize", onResize);
237
+ return () => {
238
+ cancelAnimationFrame(frame);
239
+ listElement.removeEventListener("scroll", onScroll);
240
+ window.removeEventListener("resize", onResize);
241
+ observer?.disconnect();
242
+ };
243
+ }, [showSlidingIndicator, syncIndicator, items.length]);
244
+ React.useEffect(() => {
245
+ if (!hasInteracted || motionPreset === "none" || effectiveMotionDuration <= 0) {
246
+ return;
247
+ }
248
+ const panelElement = activePanelRef.current;
249
+ if (!panelElement || typeof panelElement.animate !== "function") {
250
+ return;
251
+ }
252
+ if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
253
+ return;
254
+ }
255
+ const offsetX = motionPreset === "expressive" ? panelDirection === "forward" ? 20 : panelDirection === "backward" ? -20 : 0 : 0;
256
+ const offsetY = motionPreset === "expressive" ? 10 : 6;
257
+ const startScale = motionPreset === "expressive" ? 0.985 : 0.995;
258
+ const animation = panelElement.animate(
259
+ [
260
+ {
261
+ opacity: 0,
262
+ transform: `translate3d(${offsetX}px, ${offsetY}px, 0) scale(${startScale})`,
263
+ filter: "blur(1px)"
264
+ },
265
+ {
266
+ opacity: 1,
267
+ transform: "translate3d(0, 0, 0) scale(1)",
268
+ filter: "blur(0px)"
269
+ }
270
+ ],
271
+ {
272
+ duration: effectiveMotionDuration,
273
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
274
+ fill: "both"
275
+ }
276
+ );
277
+ return () => {
278
+ animation.cancel();
279
+ };
280
+ }, [
281
+ selectedIndex,
282
+ hasInteracted,
283
+ motionPreset,
284
+ panelDirection,
285
+ effectiveMotionDuration
286
+ ]);
287
+ const handleTabKeyDown = React.useCallback(
288
+ (event, index) => {
289
+ if (!enabledIndices.length) return;
290
+ switch (event.key) {
291
+ case "ArrowRight": {
292
+ event.preventDefault();
293
+ const next = getNextEnabledIndex(enabledIndices, index, 1);
294
+ if (next !== -1) {
295
+ focusTabIndex(next);
296
+ if (activationMode === "automatic") selectTab(next);
297
+ }
298
+ break;
299
+ }
300
+ case "ArrowLeft": {
301
+ event.preventDefault();
302
+ const previous = getNextEnabledIndex(enabledIndices, index, -1);
303
+ if (previous !== -1) {
304
+ focusTabIndex(previous);
305
+ if (activationMode === "automatic") selectTab(previous);
306
+ }
307
+ break;
308
+ }
309
+ case "Home": {
310
+ event.preventDefault();
311
+ const first = enabledIndices[0];
312
+ if (first !== void 0) {
313
+ focusTabIndex(first);
314
+ if (activationMode === "automatic") selectTab(first);
315
+ }
316
+ break;
317
+ }
318
+ case "End": {
319
+ event.preventDefault();
320
+ const last = enabledIndices[enabledIndices.length - 1];
321
+ if (last !== void 0) {
322
+ focusTabIndex(last);
323
+ if (activationMode === "automatic") selectTab(last);
324
+ }
325
+ break;
326
+ }
327
+ case "Enter":
328
+ case " ":
329
+ case "Spacebar": {
330
+ if (activationMode === "manual") {
331
+ event.preventDefault();
332
+ selectTab(index);
333
+ }
334
+ break;
335
+ }
336
+ default:
337
+ break;
338
+ }
339
+ },
340
+ [activationMode, enabledIndices, focusTabIndex, selectTab]
341
+ );
342
+ const themedStyle = React.useMemo(() => {
343
+ const cssVars = { ...style };
344
+ if (radius !== void 0) {
345
+ cssVars["--arc-radius"] = `${radius}px`;
346
+ }
347
+ if (gap !== void 0) {
348
+ cssVars["--arc-gap"] = `${gap}px`;
349
+ }
350
+ const panelPaddingValue = toCssSize(panelPadding);
351
+ if (panelPaddingValue !== void 0) {
352
+ cssVars["--arc-panel-padding"] = panelPaddingValue;
353
+ }
354
+ if (accentColor) {
355
+ cssVars["--arc-accent"] = accentColor;
356
+ }
357
+ if (tabBackground) {
358
+ cssVars["--arc-tab-bg"] = tabBackground;
359
+ }
360
+ if (tabHoverBackground) {
361
+ cssVars["--arc-tab-hover-bg"] = tabHoverBackground;
362
+ }
363
+ if (panelBackground) {
364
+ cssVars["--arc-panel-bg"] = panelBackground;
365
+ }
366
+ if (panelBorderColor) {
367
+ cssVars["--arc-panel-border"] = panelBorderColor;
368
+ }
369
+ cssVars["--arc-motion-duration"] = `${effectiveMotionDuration}ms`;
370
+ return cssVars;
371
+ }, [
372
+ style,
373
+ radius,
374
+ gap,
375
+ panelPadding,
376
+ accentColor,
377
+ tabBackground,
378
+ tabHoverBackground,
379
+ panelBackground,
380
+ panelBorderColor,
381
+ effectiveMotionDuration
382
+ ]);
383
+ const rootClassName = joinClassNames(
384
+ "arc-tabs",
385
+ `arc-tabs--size-${size}`,
386
+ `arc-tabs--fit-${fit}`,
387
+ `arc-tabs--motion-${motionPreset}`,
388
+ hasInteracted && "arc-tabs--has-interacted",
389
+ panelDirection !== "none" && `arc-tabs--direction-${panelDirection}`,
390
+ className
391
+ );
392
+ const indicatorStyle = React.useMemo(
393
+ () => ({
394
+ "--arc-indicator-x": `${indicator.x}px`,
395
+ "--arc-indicator-w": `${indicator.width}px`
396
+ }),
397
+ [indicator.x, indicator.width]
398
+ );
399
+ const renderDefaultLabel = (item) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
400
+ item.icon ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "arc-tabs__icon", children: item.icon }) : null,
401
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "arc-tabs__text", children: item.label }),
402
+ item.badge !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "arc-tabs__badge", children: item.badge }) : null
403
+ ] });
404
+ const renderPanelContent = (item, state) => renderPanel ? renderPanel(item, state) : item.content;
405
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: rootClassName, style: themedStyle, ...rest, children: [
406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
407
+ "ul",
408
+ {
409
+ ref: listRef,
410
+ className: joinClassNames("arc-tabs__list", tabsClassName),
411
+ role: "tablist",
412
+ "aria-label": ariaLabel,
413
+ id: `${baseId}-list`,
414
+ children: [
415
+ showSlidingIndicator ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
416
+ "li",
417
+ {
418
+ "aria-hidden": "true",
419
+ role: "presentation",
420
+ className: joinClassNames(
421
+ "arc-tabs__active-indicator",
422
+ indicator.ready && "is-ready"
423
+ ),
424
+ style: indicatorStyle
425
+ }
426
+ ) : null,
427
+ items.map((item, index) => {
428
+ const selected = index === selectedIndex;
429
+ const disabled = Boolean(item.disabled);
430
+ const tabId = `${baseId}-tab-${index}`;
431
+ const panelId = `${baseId}-panel-${index}`;
432
+ const state = { index, selected, disabled };
433
+ const tabIndexValue = disabled ? -1 : focusedIndex === index || focusedIndex === -1 && selected ? 0 : -1;
434
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "arc-tabs__item", role: "presentation", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
435
+ "button",
436
+ {
437
+ id: tabId,
438
+ ref: (node) => {
439
+ tabRefs.current[index] = node;
440
+ },
441
+ type: "button",
442
+ role: "tab",
443
+ "aria-selected": selected,
444
+ "aria-controls": panelId,
445
+ tabIndex: tabIndexValue,
446
+ disabled,
447
+ className: "arc-tabs__tab",
448
+ onFocus: () => setFocusedIndex(index),
449
+ onClick: () => selectTab(index),
450
+ onKeyDown: (event) => handleTabKeyDown(event, index),
451
+ children: renderTabLabel ? renderTabLabel(item, state) : renderDefaultLabel(item)
452
+ }
453
+ ) }, item.id);
454
+ })
455
+ ]
456
+ }
457
+ ),
458
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: joinClassNames("arc-tabs__panels", panelClassName), children: [
459
+ items.length === 0 && emptyState,
460
+ items.length > 0 && selectedItem === void 0 && emptyState,
461
+ items.length > 0 && selectedItem !== void 0 && keepMounted ? items.map((item, index) => {
462
+ const selected = index === selectedIndex;
463
+ const disabled = Boolean(item.disabled);
464
+ const tabId = `${baseId}-tab-${index}`;
465
+ const panelId = `${baseId}-panel-${index}`;
466
+ const state = { index, selected, disabled };
467
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
468
+ "section",
469
+ {
470
+ ref: (node) => {
471
+ if (selected) {
472
+ activePanelRef.current = node;
473
+ }
474
+ },
475
+ id: panelId,
476
+ className: "arc-tabs__panel",
477
+ role: "tabpanel",
478
+ "aria-labelledby": tabId,
479
+ "aria-hidden": !selected,
480
+ hidden: !selected,
481
+ children: renderPanelContent(item, state)
482
+ },
483
+ item.id
484
+ );
485
+ }) : null,
486
+ items.length > 0 && selectedItem !== void 0 && !keepMounted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
487
+ "section",
488
+ {
489
+ ref: (node) => {
490
+ activePanelRef.current = node;
491
+ },
492
+ id: `${baseId}-panel-${selectedIndex}`,
493
+ className: "arc-tabs__panel",
494
+ role: "tabpanel",
495
+ "aria-labelledby": `${baseId}-tab-${selectedIndex}`,
496
+ "aria-hidden": false,
497
+ children: renderPanelContent(selectedItem, {
498
+ index: selectedIndex,
499
+ selected: true,
500
+ disabled: Boolean(selectedItem.disabled)
501
+ })
502
+ }
503
+ ) : null
504
+ ] })
505
+ ] });
506
+ }
507
+
508
+ // src/ArcTabsTailwind.tsx
509
+ var React2 = __toESM(require("react"), 1);
510
+ var import_jsx_runtime2 = require("react/jsx-runtime");
511
+ var joinClassNames2 = (...parts) => parts.filter(Boolean).join(" ");
512
+ var toCssSize2 = (value) => typeof value === "number" ? `${value}px` : value;
513
+ var findFirstEnabledIndex2 = (items) => items.findIndex((item) => !item.disabled);
514
+ var getEnabledIndices2 = (items) => items.reduce((acc, item, index) => {
515
+ if (!item.disabled) acc.push(index);
516
+ return acc;
517
+ }, []);
518
+ var getNextEnabledIndex2 = (enabledIndices, currentIndex, direction) => {
519
+ if (!enabledIndices.length) return -1;
520
+ const currentPosition = enabledIndices.indexOf(currentIndex);
521
+ if (currentPosition === -1) {
522
+ return direction === 1 ? enabledIndices[0] ?? -1 : enabledIndices[enabledIndices.length - 1] ?? -1;
523
+ }
524
+ const nextPosition = (currentPosition + direction + enabledIndices.length) % enabledIndices.length;
525
+ return enabledIndices[nextPosition] ?? -1;
526
+ };
527
+ var sizeClassMap = {
528
+ sm: "min-h-9 px-3 py-1.5 text-sm",
529
+ md: "min-h-10 px-4 py-2 text-[0.95rem]",
530
+ lg: "min-h-12 px-5 py-2.5 text-base"
531
+ };
532
+ function ArcTabsTailwind({
533
+ items,
534
+ value,
535
+ defaultValue,
536
+ onValueChange,
537
+ activationMode = "automatic",
538
+ keepMounted = true,
539
+ size = "md",
540
+ fit = "content",
541
+ motionPreset = "subtle",
542
+ motionDuration = 260,
543
+ ariaLabel = "Tabs",
544
+ listId,
545
+ tabsClassName,
546
+ panelClassName,
547
+ radius,
548
+ gap,
549
+ panelPadding,
550
+ accentColor,
551
+ tabBackground,
552
+ tabHoverBackground,
553
+ panelBackground,
554
+ panelBorderColor,
555
+ emptyState = null,
556
+ renderTabLabel,
557
+ renderPanel,
558
+ className,
559
+ style,
560
+ classNames,
561
+ ...rest
562
+ }) {
563
+ const reactId = React2.useId();
564
+ const baseId = React2.useMemo(
565
+ () => (listId ?? `arc-tabs-${reactId}`).replace(/:/g, ""),
566
+ [listId, reactId]
567
+ );
568
+ const isControlled = value !== void 0;
569
+ const firstEnabledIndex = React2.useMemo(
570
+ () => findFirstEnabledIndex2(items),
571
+ [items]
572
+ );
573
+ const [uncontrolledValue, setUncontrolledValue] = React2.useState(() => {
574
+ const requested = defaultValue;
575
+ const requestedMatch = items.find(
576
+ (item) => !item.disabled && item.id === requested
577
+ );
578
+ if (requestedMatch) return requestedMatch.id;
579
+ return firstEnabledIndex >= 0 ? items[firstEnabledIndex]?.id : void 0;
580
+ });
581
+ const rawValue = isControlled ? value : uncontrolledValue;
582
+ const strictSelectedIndex = React2.useMemo(
583
+ () => items.findIndex((item) => !item.disabled && item.id === rawValue),
584
+ [items, rawValue]
585
+ );
586
+ const selectedIndex = strictSelectedIndex >= 0 ? strictSelectedIndex : firstEnabledIndex;
587
+ const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : void 0;
588
+ React2.useEffect(() => {
589
+ if (isControlled) return;
590
+ if (strictSelectedIndex !== -1) return;
591
+ if (firstEnabledIndex !== -1) {
592
+ const fallbackId = items[firstEnabledIndex]?.id;
593
+ setUncontrolledValue(fallbackId);
594
+ } else {
595
+ setUncontrolledValue(void 0);
596
+ }
597
+ }, [isControlled, strictSelectedIndex, firstEnabledIndex, items]);
598
+ const [focusedIndex, setFocusedIndex] = React2.useState(selectedIndex);
599
+ React2.useEffect(() => {
600
+ if (selectedIndex === -1) {
601
+ setFocusedIndex(-1);
602
+ return;
603
+ }
604
+ if (focusedIndex < 0 || focusedIndex >= items.length || items[focusedIndex]?.disabled) {
605
+ setFocusedIndex(selectedIndex);
606
+ }
607
+ }, [focusedIndex, selectedIndex, items]);
608
+ const enabledIndices = React2.useMemo(() => getEnabledIndices2(items), [items]);
609
+ const tabRefs = React2.useRef([]);
610
+ const listRef = React2.useRef(null);
611
+ const activePanelRef = React2.useRef(null);
612
+ const hasMountedRef = React2.useRef(false);
613
+ const previousSelectedIndexRef = React2.useRef(selectedIndex);
614
+ const [hasInteracted, setHasInteracted] = React2.useState(false);
615
+ const [panelDirection, setPanelDirection] = React2.useState("none");
616
+ const [indicator, setIndicator] = React2.useState({
617
+ x: 0,
618
+ width: 0,
619
+ ready: false
620
+ });
621
+ const effectiveMotionDuration = motionPreset === "none" ? 0 : Math.max(0, motionDuration);
622
+ const showSlidingIndicator = motionPreset === "expressive" && selectedIndex >= 0;
623
+ React2.useEffect(() => {
624
+ tabRefs.current = tabRefs.current.slice(0, items.length);
625
+ }, [items.length]);
626
+ React2.useEffect(() => {
627
+ const previous = previousSelectedIndexRef.current;
628
+ if (!hasMountedRef.current) {
629
+ hasMountedRef.current = true;
630
+ previousSelectedIndexRef.current = selectedIndex;
631
+ return;
632
+ }
633
+ if (previous !== selectedIndex) {
634
+ setHasInteracted(true);
635
+ if (selectedIndex >= 0 && previous >= 0) {
636
+ setPanelDirection(selectedIndex > previous ? "forward" : "backward");
637
+ }
638
+ }
639
+ previousSelectedIndexRef.current = selectedIndex;
640
+ }, [selectedIndex]);
641
+ const focusTabIndex = React2.useCallback((index) => {
642
+ if (index < 0) return;
643
+ setFocusedIndex(index);
644
+ tabRefs.current[index]?.focus();
645
+ }, []);
646
+ const selectTab = React2.useCallback(
647
+ (index) => {
648
+ const item = items[index];
649
+ if (!item || item.disabled) return;
650
+ if (index === selectedIndex) {
651
+ setFocusedIndex(index);
652
+ return;
653
+ }
654
+ setHasInteracted(true);
655
+ if (selectedIndex >= 0) {
656
+ setPanelDirection(index > selectedIndex ? "forward" : "backward");
657
+ }
658
+ if (!isControlled) {
659
+ setUncontrolledValue(item.id);
660
+ }
661
+ setFocusedIndex(index);
662
+ onValueChange?.(item.id, item, index);
663
+ },
664
+ [items, selectedIndex, isControlled, onValueChange]
665
+ );
666
+ const syncIndicator = React2.useCallback(() => {
667
+ if (!showSlidingIndicator) {
668
+ setIndicator(
669
+ (previous) => previous.ready || previous.width !== 0 || previous.x !== 0 ? { x: 0, width: 0, ready: false } : previous
670
+ );
671
+ return;
672
+ }
673
+ const listElement = listRef.current;
674
+ const selectedTab = selectedIndex >= 0 ? tabRefs.current[selectedIndex] ?? null : null;
675
+ if (!listElement || !selectedTab) return;
676
+ const listRect = listElement.getBoundingClientRect();
677
+ const tabRect = selectedTab.getBoundingClientRect();
678
+ const nextX = tabRect.left - listRect.left + listElement.scrollLeft;
679
+ const nextWidth = tabRect.width;
680
+ setIndicator((previous) => {
681
+ const changedX = Math.abs(previous.x - nextX) > 0.5;
682
+ const changedWidth = Math.abs(previous.width - nextWidth) > 0.5;
683
+ if (!changedX && !changedWidth && previous.ready) {
684
+ return previous;
685
+ }
686
+ return {
687
+ x: nextX,
688
+ width: nextWidth,
689
+ ready: true
690
+ };
691
+ });
692
+ }, [selectedIndex, showSlidingIndicator]);
693
+ React2.useEffect(() => {
694
+ syncIndicator();
695
+ }, [syncIndicator, items.length, size, fit]);
696
+ React2.useEffect(() => {
697
+ if (!showSlidingIndicator) return;
698
+ const listElement = listRef.current;
699
+ if (!listElement) return;
700
+ const onResize = () => syncIndicator();
701
+ const onScroll = () => syncIndicator();
702
+ const frame = requestAnimationFrame(syncIndicator);
703
+ let observer = null;
704
+ if (typeof ResizeObserver !== "undefined") {
705
+ observer = new ResizeObserver(() => syncIndicator());
706
+ observer.observe(listElement);
707
+ tabRefs.current.forEach((tabElement) => {
708
+ if (tabElement) observer?.observe(tabElement);
709
+ });
710
+ }
711
+ listElement.addEventListener("scroll", onScroll, { passive: true });
712
+ window.addEventListener("resize", onResize);
713
+ return () => {
714
+ cancelAnimationFrame(frame);
715
+ listElement.removeEventListener("scroll", onScroll);
716
+ window.removeEventListener("resize", onResize);
717
+ observer?.disconnect();
718
+ };
719
+ }, [showSlidingIndicator, syncIndicator, items.length]);
720
+ React2.useEffect(() => {
721
+ if (!hasInteracted || motionPreset === "none" || effectiveMotionDuration <= 0) {
722
+ return;
723
+ }
724
+ const panelElement = activePanelRef.current;
725
+ if (!panelElement || typeof panelElement.animate !== "function") {
726
+ return;
727
+ }
728
+ if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
729
+ return;
730
+ }
731
+ const offsetX = motionPreset === "expressive" ? panelDirection === "forward" ? 20 : panelDirection === "backward" ? -20 : 0 : 0;
732
+ const offsetY = motionPreset === "expressive" ? 10 : 6;
733
+ const startScale = motionPreset === "expressive" ? 0.985 : 0.995;
734
+ const animation = panelElement.animate(
735
+ [
736
+ {
737
+ opacity: 0,
738
+ transform: `translate3d(${offsetX}px, ${offsetY}px, 0) scale(${startScale})`,
739
+ filter: "blur(1px)"
740
+ },
741
+ {
742
+ opacity: 1,
743
+ transform: "translate3d(0, 0, 0) scale(1)",
744
+ filter: "blur(0px)"
745
+ }
746
+ ],
747
+ {
748
+ duration: effectiveMotionDuration,
749
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
750
+ fill: "both"
751
+ }
752
+ );
753
+ return () => {
754
+ animation.cancel();
755
+ };
756
+ }, [
757
+ selectedIndex,
758
+ hasInteracted,
759
+ motionPreset,
760
+ panelDirection,
761
+ effectiveMotionDuration
762
+ ]);
763
+ const handleTabKeyDown = React2.useCallback(
764
+ (event, index) => {
765
+ if (!enabledIndices.length) return;
766
+ switch (event.key) {
767
+ case "ArrowRight": {
768
+ event.preventDefault();
769
+ const next = getNextEnabledIndex2(enabledIndices, index, 1);
770
+ if (next !== -1) {
771
+ focusTabIndex(next);
772
+ if (activationMode === "automatic") selectTab(next);
773
+ }
774
+ break;
775
+ }
776
+ case "ArrowLeft": {
777
+ event.preventDefault();
778
+ const previous = getNextEnabledIndex2(enabledIndices, index, -1);
779
+ if (previous !== -1) {
780
+ focusTabIndex(previous);
781
+ if (activationMode === "automatic") selectTab(previous);
782
+ }
783
+ break;
784
+ }
785
+ case "Home": {
786
+ event.preventDefault();
787
+ const first = enabledIndices[0];
788
+ if (first !== void 0) {
789
+ focusTabIndex(first);
790
+ if (activationMode === "automatic") selectTab(first);
791
+ }
792
+ break;
793
+ }
794
+ case "End": {
795
+ event.preventDefault();
796
+ const last = enabledIndices[enabledIndices.length - 1];
797
+ if (last !== void 0) {
798
+ focusTabIndex(last);
799
+ if (activationMode === "automatic") selectTab(last);
800
+ }
801
+ break;
802
+ }
803
+ case "Enter":
804
+ case " ":
805
+ case "Spacebar": {
806
+ if (activationMode === "manual") {
807
+ event.preventDefault();
808
+ selectTab(index);
809
+ }
810
+ break;
811
+ }
812
+ default:
813
+ break;
814
+ }
815
+ },
816
+ [activationMode, enabledIndices, focusTabIndex, selectTab]
817
+ );
818
+ const themedStyle = React2.useMemo(() => {
819
+ const cssVars = { ...style };
820
+ if (radius !== void 0) {
821
+ cssVars["--arc-radius"] = `${radius}px`;
822
+ }
823
+ if (gap !== void 0) {
824
+ cssVars["--arc-gap"] = `${gap}px`;
825
+ }
826
+ const panelPaddingValue = toCssSize2(panelPadding);
827
+ if (panelPaddingValue !== void 0) {
828
+ cssVars["--arc-panel-padding"] = panelPaddingValue;
829
+ }
830
+ if (accentColor) {
831
+ cssVars["--arc-accent"] = accentColor;
832
+ }
833
+ if (tabBackground) {
834
+ cssVars["--arc-tab-bg"] = tabBackground;
835
+ }
836
+ if (tabHoverBackground) {
837
+ cssVars["--arc-tab-hover-bg"] = tabHoverBackground;
838
+ }
839
+ if (panelBackground) {
840
+ cssVars["--arc-panel-bg"] = panelBackground;
841
+ }
842
+ if (panelBorderColor) {
843
+ cssVars["--arc-panel-border"] = panelBorderColor;
844
+ }
845
+ cssVars["--arc-motion-duration"] = `${effectiveMotionDuration}ms`;
846
+ return cssVars;
847
+ }, [
848
+ style,
849
+ radius,
850
+ gap,
851
+ panelPadding,
852
+ accentColor,
853
+ tabBackground,
854
+ tabHoverBackground,
855
+ panelBackground,
856
+ panelBorderColor,
857
+ effectiveMotionDuration
858
+ ]);
859
+ const rootClassName = joinClassNames2(
860
+ "arc-tabs-tw w-full text-[var(--arc-text)] [--arc-radius:14px] [--arc-gap:10px] [--arc-border-width:1px] [--arc-accent:#5b4ff1] [--arc-text:#171a2c] [--arc-tab-bg:#e7ebff] [--arc-tab-hover-bg:#dce3ff] [--arc-panel-bg:#ffffff] [--arc-panel-border:#cfd6f5] [--arc-panel-padding:1rem] [--arc-motion-duration:260ms] dark:[--arc-text:#edf1ff] dark:[--arc-tab-bg:#2c3555] dark:[--arc-tab-hover-bg:#374268] dark:[--arc-panel-bg:#1c243b] dark:[--arc-panel-border:#46527e]",
861
+ classNames?.root,
862
+ className
863
+ );
864
+ const listClassName = joinClassNames2(
865
+ "relative m-0 flex list-none items-end gap-[var(--arc-gap)] overflow-x-auto overflow-y-clip p-0 pb-[var(--arc-gap)] isolate",
866
+ classNames?.list,
867
+ tabsClassName
868
+ );
869
+ const panelsClassName = joinClassNames2(
870
+ "relative z-[2] mt-0 rounded-[var(--arc-radius)] border border-[var(--arc-panel-border)] bg-[var(--arc-panel-bg)] p-[var(--arc-panel-padding)] shadow-[0_12px_32px_rgba(15,23,42,0.12)]",
871
+ classNames?.panels,
872
+ panelClassName
873
+ );
874
+ const renderDefaultLabel = (item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
875
+ item.icon ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: joinClassNames2("inline-flex leading-none", classNames?.icon), children: item.icon }) : null,
876
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: joinClassNames2("inline-block", classNames?.text), children: item.label }),
877
+ item.badge !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
878
+ "span",
879
+ {
880
+ className: joinClassNames2(
881
+ "inline-flex min-w-5 items-center justify-center rounded-full bg-[var(--arc-accent)] px-1.5 py-0.5 text-[0.72em] font-bold text-white/95",
882
+ classNames?.badge
883
+ ),
884
+ children: item.badge
885
+ }
886
+ ) : null
887
+ ] });
888
+ const renderPanelContent = (item, state) => renderPanel ? renderPanel(item, state) : item.content;
889
+ const indicatorStyle = React2.useMemo(
890
+ () => ({
891
+ "--arc-indicator-x": `${indicator.x}px`,
892
+ "--arc-indicator-w": `${indicator.width}px`
893
+ }),
894
+ [indicator.x, indicator.width]
895
+ );
896
+ const indicatorClassName = joinClassNames2(
897
+ "pointer-events-none absolute left-0 top-0 z-[1] h-[calc(100%-var(--arc-gap))] w-[var(--arc-indicator-w)] translate-x-[var(--arc-indicator-x)] rounded-t-[var(--arc-radius)] rounded-b-none bg-[var(--arc-panel-bg)] opacity-0 [box-shadow:0_calc(var(--arc-gap)+var(--arc-border-width))_0_var(--arc-panel-bg)] transition-[transform,width,opacity] [transition-duration:var(--arc-motion-duration)] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]",
898
+ indicator.ready && "opacity-100",
899
+ classNames?.indicator
900
+ );
901
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: rootClassName, style: themedStyle, "data-slot": "root", ...rest, children: [
902
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
903
+ "ul",
904
+ {
905
+ ref: listRef,
906
+ className: listClassName,
907
+ role: "tablist",
908
+ "aria-label": ariaLabel,
909
+ id: `${baseId}-list`,
910
+ "data-slot": "list",
911
+ children: [
912
+ showSlidingIndicator ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
913
+ "li",
914
+ {
915
+ "aria-hidden": "true",
916
+ role: "presentation",
917
+ className: indicatorClassName,
918
+ style: indicatorStyle,
919
+ "data-slot": "indicator"
920
+ }
921
+ ) : null,
922
+ items.map((item, index) => {
923
+ const selected = index === selectedIndex;
924
+ const disabled = Boolean(item.disabled);
925
+ const tabId = `${baseId}-tab-${index}`;
926
+ const panelId = `${baseId}-panel-${index}`;
927
+ const state = { index, selected, disabled };
928
+ const tabIndexValue = disabled ? -1 : focusedIndex === index || focusedIndex === -1 && selected ? 0 : -1;
929
+ const itemClassName = joinClassNames2(
930
+ fit === "equal" ? "relative z-[2] min-w-0 flex-1" : "relative z-[2] shrink-0",
931
+ classNames?.item
932
+ );
933
+ const tabClassName = joinClassNames2(
934
+ "relative inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--arc-radius)] border border-[var(--arc-panel-border)] bg-[var(--arc-tab-bg)] text-inherit font-semibold leading-none select-none transition-[background-color,color,transform,border-color,box-shadow] [transition-duration:var(--arc-motion-duration)] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--arc-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--arc-panel-bg)] disabled:cursor-not-allowed disabled:opacity-45 before:pointer-events-none before:absolute before:bottom-0 before:left-[calc(var(--arc-border-width)*-1)] before:h-[calc(var(--arc-radius)*2)] before:w-[calc(var(--arc-radius)+var(--arc-border-width))] before:translate-y-[calc(var(--arc-gap)+var(--arc-border-width))] before:bg-transparent before:opacity-0 before:content-[''] before:[box-shadow:0_var(--arc-radius)_0_var(--arc-panel-bg)] before:transition-[opacity,transform] before:[transition-duration:var(--arc-motion-duration)] before:[transition-timing-function:cubic-bezier(0.22,1,0.36,1)] after:pointer-events-none after:absolute after:bottom-0 after:right-[calc(var(--arc-border-width)*-1)] after:h-[calc(var(--arc-radius)*2)] after:w-[calc(var(--arc-radius)+var(--arc-border-width))] after:translate-y-[calc(var(--arc-gap)+var(--arc-border-width))] after:bg-transparent after:opacity-0 after:content-[''] after:[box-shadow:0_var(--arc-radius)_0_var(--arc-panel-bg)] after:transition-[opacity,transform] after:[transition-duration:var(--arc-motion-duration)] after:[transition-timing-function:cubic-bezier(0.22,1,0.36,1)]",
935
+ sizeClassMap[size],
936
+ fit === "equal" && "w-full justify-center",
937
+ selected ? joinClassNames2(
938
+ "z-[3] rounded-b-none border-[var(--arc-panel-bg)] text-[var(--arc-accent)] before:opacity-100 after:opacity-100 [box-shadow:0_calc(var(--arc-gap)+var(--arc-border-width))_0_var(--arc-panel-bg)]",
939
+ motionPreset === "expressive" ? "bg-transparent" : "bg-[var(--arc-panel-bg)]"
940
+ ) : "enabled:hover:bg-[var(--arc-tab-hover-bg)] enabled:hover:translate-y-px enabled:active:translate-y-[2px]",
941
+ index > 0 && "before:-translate-x-full before:translate-y-[calc(var(--arc-gap)+var(--arc-border-width))] before:rounded-br-[var(--arc-radius)] before:[box-shadow:var(--arc-border-width)_var(--arc-radius)_0_var(--arc-panel-bg)]",
942
+ index < items.length - 1 && "after:translate-x-full after:translate-y-[calc(var(--arc-gap)+var(--arc-border-width))] after:rounded-bl-[var(--arc-radius)] after:[box-shadow:calc(var(--arc-border-width)*-1)_var(--arc-radius)_0_var(--arc-panel-bg)]",
943
+ selected ? classNames?.tabSelected : classNames?.tabUnselected,
944
+ disabled && classNames?.tabDisabled,
945
+ classNames?.tab
946
+ );
947
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { className: itemClassName, role: "presentation", "data-slot": "item", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
948
+ "button",
949
+ {
950
+ id: tabId,
951
+ ref: (node) => {
952
+ tabRefs.current[index] = node;
953
+ },
954
+ type: "button",
955
+ role: "tab",
956
+ "aria-selected": selected,
957
+ "aria-controls": panelId,
958
+ tabIndex: tabIndexValue,
959
+ disabled,
960
+ className: tabClassName,
961
+ onFocus: () => setFocusedIndex(index),
962
+ onClick: () => selectTab(index),
963
+ onKeyDown: (event) => handleTabKeyDown(event, index),
964
+ "data-slot": "tab",
965
+ children: renderTabLabel ? renderTabLabel(item, state) : renderDefaultLabel(item)
966
+ }
967
+ ) }, item.id);
968
+ })
969
+ ]
970
+ }
971
+ ),
972
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: panelsClassName, "data-slot": "panels", children: [
973
+ items.length === 0 && emptyState,
974
+ items.length > 0 && selectedItem === void 0 && emptyState,
975
+ items.length > 0 && selectedItem !== void 0 && keepMounted ? items.map((item, index) => {
976
+ const selected = index === selectedIndex;
977
+ const disabled = Boolean(item.disabled);
978
+ const tabId = `${baseId}-tab-${index}`;
979
+ const panelId = `${baseId}-panel-${index}`;
980
+ const state = { index, selected, disabled };
981
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
982
+ "section",
983
+ {
984
+ ref: (node) => {
985
+ if (selected) {
986
+ activePanelRef.current = node;
987
+ }
988
+ },
989
+ id: panelId,
990
+ className: joinClassNames2("outline-none", classNames?.panel),
991
+ role: "tabpanel",
992
+ "aria-labelledby": tabId,
993
+ "aria-hidden": !selected,
994
+ hidden: !selected,
995
+ "data-slot": "panel",
996
+ children: renderPanelContent(item, state)
997
+ },
998
+ item.id
999
+ );
1000
+ }) : null,
1001
+ items.length > 0 && selectedItem !== void 0 && !keepMounted ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1002
+ "section",
1003
+ {
1004
+ ref: (node) => {
1005
+ activePanelRef.current = node;
1006
+ },
1007
+ id: `${baseId}-panel-${selectedIndex}`,
1008
+ className: joinClassNames2("outline-none", classNames?.panel),
1009
+ role: "tabpanel",
1010
+ "aria-labelledby": `${baseId}-tab-${selectedIndex}`,
1011
+ "aria-hidden": false,
1012
+ "data-slot": "panel",
1013
+ children: renderPanelContent(selectedItem, {
1014
+ index: selectedIndex,
1015
+ selected: true,
1016
+ disabled: Boolean(selectedItem.disabled)
1017
+ })
1018
+ }
1019
+ ) : null
1020
+ ] })
1021
+ ] });
1022
+ }
1023
+ // Annotate the CommonJS export names for ESM import in node:
1024
+ 0 && (module.exports = {
1025
+ ArcTabs,
1026
+ ArcTabsTailwind
1027
+ });
1028
+ //# sourceMappingURL=index.cjs.map