@entro314labs/react-arc-tabs 1.0.0 → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 entro314-labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,43 +1,32 @@
1
- # react-arc-tabs
1
+ # @entro314labs/react-arc-tabs
2
2
 
3
- Production-ready, reusable arc-style tabs for React and Next.js.
3
+ Reusable arc-style tabs component for React and Next.js, including a Tailwind CSS v4+ variant.
4
4
 
5
- This component is based on the shared visual mechanics from the demos in this workspace:
5
+ ## Highlights
6
6
 
7
- - active tab merges into the panel
8
- - inverse curved bottom corners on the active tab seam
9
- - layered z-index and pseudo-element geometry for seamless arc transitions
10
- - keyboard and ARIA-compliant tab interactions
7
+ - Accessible tabs: ARIA roles + keyboard interactions
8
+ - Controlled and uncontrolled state models
9
+ - CSS-based and Tailwind v4+ variants
10
+ - Switch animations (including sliding active backdrop and animated panel entry)
11
+ - TypeScript-first API
11
12
 
12
- ## Features
13
+ ## Compatibility
13
14
 
14
- - Controlled and uncontrolled usage
15
- - Full keyboard navigation (`ArrowLeft`, `ArrowRight`, `Home`, `End`, `Enter`, `Space`)
16
- - Disabled tabs support
17
- - Manual or automatic activation mode
18
- - Keep mounted or render-only-active panel strategies
19
- - Themeable with CSS variables and props
20
- - Two styling engines:
21
- - `ArcTabs` (CSS file based)
22
- - `ArcTabsTailwind` (Tailwind CSS v4+ utility based)
23
- - Built-in tab switch animations adapted from the demos:
24
- - sliding active backdrop (`rounded-tab` inspired)
25
- - smooth seam/corner transitions (`inverse rounded corners` inspired)
26
- - animated panel entry with directional motion
27
- - Works in React and Next.js (`"use client"` included)
15
+ - React `18.2+` and `19+`
16
+ - Next.js (App Router and Pages Router)
17
+ - Tailwind CSS `4+` for `ArcTabsTailwind`
28
18
 
29
- ## Install (local package / workspace package)
19
+ ## Install
30
20
 
31
21
  ```bash
32
- npm install
33
- npm run build
22
+ npm install @entro314labs/react-arc-tabs
34
23
  ```
35
24
 
36
- ## Usage in React
25
+ ## Basic usage (CSS variant)
37
26
 
38
27
  ```tsx
39
- import { ArcTabs, type ArcTabItem } from "react-arc-tabs";
40
- import "react-arc-tabs/styles.css";
28
+ import { ArcTabs, type ArcTabItem } from "@entro314labs/react-arc-tabs";
29
+ import "@entro314labs/react-arc-tabs/styles.css";
41
30
 
42
31
  const items: ArcTabItem[] = [
43
32
  { id: "overview", label: "Overview", content: <div>Overview content</div> },
@@ -50,24 +39,20 @@ export function Example() {
50
39
  <ArcTabs
51
40
  items={items}
52
41
  defaultValue="overview"
53
- size="md"
54
- fit="content"
55
42
  motionPreset="expressive"
56
43
  motionDuration={320}
57
- activationMode="automatic"
58
44
  ariaLabel="Project sections"
59
- accentColor="#5b4ff1"
60
45
  />
61
46
  );
62
47
  }
63
48
  ```
64
49
 
65
- ## Usage with Tailwind CSS 4+
50
+ ## Tailwind CSS v4+ usage
66
51
 
67
- `ArcTabsTailwind` renders with Tailwind utility classes (including pseudo-element arc geometry), so you don't need to import `styles.css`.
52
+ `ArcTabsTailwind` ships with utility-class styling, so you do not import `styles.css`.
68
53
 
69
54
  ```tsx
70
- import { ArcTabsTailwind, type ArcTabItem } from "react-arc-tabs";
55
+ import { ArcTabsTailwind, type ArcTabItem } from "@entro314labs/react-arc-tabs";
71
56
 
72
57
  const items: ArcTabItem[] = [
73
58
  { id: "overview", label: "Overview", content: <div>Overview content</div> },
@@ -82,10 +67,6 @@ export function ExampleTailwind() {
82
67
  defaultValue="overview"
83
68
  fit="equal"
84
69
  motionPreset="expressive"
85
- motionDuration={320}
86
- activationMode="automatic"
87
- ariaLabel="Project sections"
88
- accentColor="#4f46e5"
89
70
  classNames={{
90
71
  root: "max-w-3xl",
91
72
  panel: "prose prose-sm max-w-none"
@@ -95,27 +76,22 @@ export function ExampleTailwind() {
95
76
  }
96
77
  ```
97
78
 
98
- ### Tailwind v4 source scanning (important)
79
+ ### Tailwind source scanning
99
80
 
100
- If Tailwind doesn't generate the component classes from node modules in your setup, add an explicit source path in your global stylesheet:
81
+ If your setup does not automatically scan classes from installed dependencies, add:
101
82
 
102
83
  ```css
103
84
  @import "tailwindcss";
104
-
105
- /* npm/yarn/pnpm install use-case */
106
- @source "../node_modules/react-arc-tabs/dist/**/*.{js,cjs}";
107
-
108
- /* monorepo/local workspace use-case (adjust path) */
109
- /* @source "../../packages/react-arc-tabs/src/**/*.{ts,tsx}"; */
85
+ @source "../node_modules/@entro314labs/react-arc-tabs/dist/**/*.{js,cjs}";
110
86
  ```
111
87
 
112
- ## Usage in Next.js App Router
88
+ ## Next.js usage
113
89
 
114
90
  ```tsx
115
91
  "use client";
116
92
 
117
- import { ArcTabs, type ArcTabItem } from "react-arc-tabs";
118
- import "react-arc-tabs/styles.css";
93
+ import { ArcTabs, type ArcTabItem } from "@entro314labs/react-arc-tabs";
94
+ import "@entro314labs/react-arc-tabs/styles.css";
119
95
 
120
96
  const tabs: ArcTabItem[] = [
121
97
  { id: "summary", label: "Summary", content: <div>Summary panel</div> },
@@ -128,40 +104,19 @@ export default function Page() {
128
104
  }
129
105
  ```
130
106
 
131
- ### Next.js + Tailwind variant
107
+ ## Motion presets
132
108
 
133
- ```tsx
134
- "use client";
109
+ - `none`: no animation
110
+ - `subtle`: minimal transitions (default)
111
+ - `expressive`: sliding active backdrop + stronger panel motion
135
112
 
136
- import { ArcTabsTailwind, type ArcTabItem } from "react-arc-tabs";
137
-
138
- const tabs: ArcTabItem[] = [
139
- { id: "summary", label: "Summary", content: <div>Summary panel</div> },
140
- { id: "billing", label: "Billing", content: <div>Billing panel</div> },
141
- { id: "usage", label: "Usage", content: <div>Usage panel</div> }
142
- ];
143
-
144
- export default function Page() {
145
- return <ArcTabsTailwind items={tabs} defaultValue="summary" ariaLabel="Account tabs" />;
146
- }
113
+ ```tsx
114
+ <ArcTabs motionPreset="expressive" motionDuration={320} />
147
115
  ```
148
116
 
149
- ## Styling model
150
-
151
- You can theme via props or by overriding CSS variables on `.arc-tabs`:
152
-
153
- - `--arc-radius`
154
- - `--arc-gap`
155
- - `--arc-accent`
156
- - `--arc-tab-bg`
157
- - `--arc-tab-hover-bg`
158
- - `--arc-panel-bg`
159
- - `--arc-panel-border`
160
- - `--arc-panel-padding`
161
-
162
- ## API
117
+ ## Main API
163
118
 
164
- `ArcTabs` props:
119
+ ### `ArcTabs` / `ArcTabsTailwind`
165
120
 
166
121
  - `items: ArcTabItem[]`
167
122
  - `value?: string`
@@ -173,39 +128,19 @@ You can theme via props or by overriding CSS variables on `.arc-tabs`:
173
128
  - `motionPreset?: "none" | "subtle" | "expressive"`
174
129
  - `motionDuration?: number`
175
130
  - `keepMounted?: boolean`
176
- - `radius?: number`
177
- - `gap?: number`
178
- - `panelPadding?: number | string`
179
- - `accentColor?: string`
180
- - `tabBackground?: string`
181
- - `tabHoverBackground?: string`
182
- - `panelBackground?: string`
183
- - `panelBorderColor?: string`
184
- - `renderTabLabel?: (item, state) => ReactNode`
185
- - `renderPanel?: (item, state) => ReactNode`
186
-
187
- `ArcTabsTailwind` props:
188
-
189
- - all `ArcTabs` props
190
- - `classNames?: ArcTabsTailwindClassNames`
191
-
192
- `ArcTabsTailwindClassNames` slots:
193
-
194
- - `root`
195
- - `list`
196
- - `indicator`
197
- - `item`
198
- - `tab`
199
- - `tabSelected`
200
- - `tabUnselected`
201
- - `tabDisabled`
202
- - `icon`
203
- - `text`
204
- - `badge`
205
- - `panels`
206
- - `panel`
207
-
208
- `ArcTabItem`:
131
+ - `ariaLabel?: string`
132
+
133
+ Additional styling/theming props are available for both variants.
134
+
135
+ ### Tailwind slot overrides
136
+
137
+ `ArcTabsTailwind` supports `classNames` slots:
138
+
139
+ - `root`, `list`, `indicator`, `item`, `tab`
140
+ - `tabSelected`, `tabUnselected`, `tabDisabled`
141
+ - `icon`, `text`, `badge`, `panels`, `panel`
142
+
143
+ ### `ArcTabItem`
209
144
 
210
145
  - `id: string`
211
146
  - `label: ReactNode`
@@ -213,3 +148,15 @@ You can theme via props or by overriding CSS variables on `.arc-tabs`:
213
148
  - `disabled?: boolean`
214
149
  - `icon?: ReactNode`
215
150
  - `badge?: ReactNode`
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ pnpm install
156
+ pnpm run typecheck
157
+ pnpm run build
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT
package/dist/ArcTabs.css CHANGED
@@ -129,12 +129,12 @@
129
129
  font-size: 1rem;
130
130
  }
131
131
 
132
- .arc-tabs__tab:not([aria-selected="true"]):not(:disabled):hover {
132
+ .arc-tabs__tab:not([aria-selected='true']):not(:disabled):hover {
133
133
  background: var(--arc-tab-hover-bg);
134
134
  transform: translateY(1px);
135
135
  }
136
136
 
137
- .arc-tabs__tab:not([aria-selected="true"]):not(:disabled):active {
137
+ .arc-tabs__tab:not([aria-selected='true']):not(:disabled):active {
138
138
  transform: translateY(2px);
139
139
  }
140
140
 
@@ -150,7 +150,7 @@
150
150
 
151
151
  .arc-tabs__tab::before,
152
152
  .arc-tabs__tab::after {
153
- content: "";
153
+ content: '';
154
154
  position: absolute;
155
155
  bottom: 0;
156
156
  width: calc(var(--arc-radius) + var(--arc-border-width));
@@ -165,8 +165,8 @@
165
165
  transform var(--arc-motion-duration) var(--arc-motion-easing);
166
166
  }
167
167
 
168
- .arc-tabs__tab[aria-selected="true"]::before,
169
- .arc-tabs__tab[aria-selected="true"]::after {
168
+ .arc-tabs__tab[aria-selected='true']::before,
169
+ .arc-tabs__tab[aria-selected='true']::after {
170
170
  opacity: 1;
171
171
  }
172
172
 
@@ -175,8 +175,7 @@
175
175
  }
176
176
 
177
177
  .arc-tabs__tab:not(:first-child)::before {
178
- transform: translateX(-100%)
179
- translateY(calc(var(--arc-gap) + var(--arc-border-width)));
178
+ transform: translateX(-100%) translateY(calc(var(--arc-gap) + var(--arc-border-width)));
180
179
  border-bottom-right-radius: var(--arc-radius);
181
180
  box-shadow: var(--arc-border-width) var(--arc-radius) 0 var(--arc-panel-bg);
182
181
  }
@@ -186,14 +185,12 @@
186
185
  }
187
186
 
188
187
  .arc-tabs__tab:not(:last-child)::after {
189
- transform: translateX(100%)
190
- translateY(calc(var(--arc-gap) + var(--arc-border-width)));
188
+ transform: translateX(100%) translateY(calc(var(--arc-gap) + var(--arc-border-width)));
191
189
  border-bottom-left-radius: var(--arc-radius);
192
- box-shadow: calc(var(--arc-border-width) * -1) var(--arc-radius) 0
193
- var(--arc-panel-bg);
190
+ box-shadow: calc(var(--arc-border-width) * -1) var(--arc-radius) 0 var(--arc-panel-bg);
194
191
  }
195
192
 
196
- .arc-tabs__tab[aria-selected="true"] {
193
+ .arc-tabs__tab[aria-selected='true'] {
197
194
  z-index: 3;
198
195
  color: color-mix(in srgb, var(--arc-accent) 85%, black 15%);
199
196
  border-color: var(--arc-panel-bg);
@@ -203,7 +200,7 @@
203
200
  box-shadow: 0 calc(var(--arc-gap) + var(--arc-border-width)) 0 var(--arc-panel-bg);
204
201
  }
205
202
 
206
- .arc-tabs--motion-expressive .arc-tabs__tab[aria-selected="true"] {
203
+ .arc-tabs--motion-expressive .arc-tabs__tab[aria-selected='true'] {
207
204
  background: transparent;
208
205
  }
209
206
 
@@ -234,7 +231,11 @@
234
231
  z-index: 2;
235
232
  margin-top: 0;
236
233
  border: var(--arc-border-width) solid var(--arc-panel-border);
237
- border-radius: var(--arc-radius);
234
+ border-top: none;
235
+ border-top-left-radius: 0;
236
+ border-top-right-radius: 0;
237
+ border-bottom-left-radius: var(--arc-radius);
238
+ border-bottom-right-radius: var(--arc-radius);
238
239
  background: var(--arc-panel-bg);
239
240
  padding: var(--arc-panel-padding);
240
241
  box-shadow: 0 12px 32px color-mix(in srgb, var(--arc-accent) 12%, transparent);
package/dist/index.cjs CHANGED
@@ -49,7 +49,7 @@ var getNextEnabledIndex = (enabledIndices, currentIndex, direction) => {
49
49
  if (!enabledIndices.length) return -1;
50
50
  const currentPosition = enabledIndices.indexOf(currentIndex);
51
51
  if (currentPosition === -1) {
52
- return direction === 1 ? enabledIndices[0] ?? -1 : enabledIndices[enabledIndices.length - 1] ?? -1;
52
+ return direction === 1 ? enabledIndices[0] ?? -1 : enabledIndices.at(-1) ?? -1;
53
53
  }
54
54
  const nextPosition = (currentPosition + direction + enabledIndices.length) % enabledIndices.length;
55
55
  return enabledIndices[nextPosition] ?? -1;
@@ -90,15 +90,10 @@ function ArcTabs({
90
90
  [listId, reactId]
91
91
  );
92
92
  const isControlled = value !== void 0;
93
- const firstEnabledIndex = React.useMemo(
94
- () => findFirstEnabledIndex(items),
95
- [items]
96
- );
93
+ const firstEnabledIndex = React.useMemo(() => findFirstEnabledIndex(items), [items]);
97
94
  const [uncontrolledValue, setUncontrolledValue] = React.useState(() => {
98
95
  const requested = defaultValue;
99
- const requestedMatch = items.find(
100
- (item) => !item.disabled && item.id === requested
101
- );
96
+ const requestedMatch = items.find((item) => !item.disabled && item.id === requested);
102
97
  if (requestedMatch) return requestedMatch.id;
103
98
  return firstEnabledIndex >= 0 ? items[firstEnabledIndex]?.id : void 0;
104
99
  });
@@ -136,7 +131,9 @@ function ArcTabs({
136
131
  const hasMountedRef = React.useRef(false);
137
132
  const previousSelectedIndexRef = React.useRef(selectedIndex);
138
133
  const [hasInteracted, setHasInteracted] = React.useState(false);
139
- const [panelDirection, setPanelDirection] = React.useState("none");
134
+ const [panelDirection, setPanelDirection] = React.useState(
135
+ "none"
136
+ );
140
137
  const [indicator, setIndicator] = React.useState({
141
138
  x: 0,
142
139
  width: 0,
@@ -221,12 +218,18 @@ function ArcTabs({
221
218
  if (!showSlidingIndicator) return;
222
219
  const listElement = listRef.current;
223
220
  if (!listElement) return;
224
- const onResize = () => syncIndicator();
225
- const onScroll = () => syncIndicator();
221
+ const onResize = () => {
222
+ syncIndicator();
223
+ };
224
+ const onScroll = () => {
225
+ syncIndicator();
226
+ };
226
227
  const frame = requestAnimationFrame(syncIndicator);
227
228
  let observer = null;
228
229
  if (typeof ResizeObserver !== "undefined") {
229
- observer = new ResizeObserver(() => syncIndicator());
230
+ observer = new ResizeObserver(() => {
231
+ syncIndicator();
232
+ });
230
233
  observer.observe(listElement);
231
234
  tabRefs.current.forEach((tabElement) => {
232
235
  if (tabElement) observer?.observe(tabElement);
@@ -249,7 +252,7 @@ function ArcTabs({
249
252
  if (!panelElement || typeof panelElement.animate !== "function") {
250
253
  return;
251
254
  }
252
- if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
255
+ if (typeof globalThis.window !== "undefined" && globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
253
256
  return;
254
257
  }
255
258
  const offsetX = motionPreset === "expressive" ? panelDirection === "forward" ? 20 : panelDirection === "backward" ? -20 : 0 : 0;
@@ -277,13 +280,7 @@ function ArcTabs({
277
280
  return () => {
278
281
  animation.cancel();
279
282
  };
280
- }, [
281
- selectedIndex,
282
- hasInteracted,
283
- motionPreset,
284
- panelDirection,
285
- effectiveMotionDuration
286
- ]);
283
+ }, [selectedIndex, hasInteracted, motionPreset, panelDirection, effectiveMotionDuration]);
287
284
  const handleTabKeyDown = React.useCallback(
288
285
  (event, index) => {
289
286
  if (!enabledIndices.length) return;
@@ -317,7 +314,7 @@ function ArcTabs({
317
314
  }
318
315
  case "End": {
319
316
  event.preventDefault();
320
- const last = enabledIndices[enabledIndices.length - 1];
317
+ const last = enabledIndices.at(-1);
321
318
  if (last !== void 0) {
322
319
  focusTabIndex(last);
323
320
  if (activationMode === "automatic") selectTab(last);
@@ -417,10 +414,7 @@ function ArcTabs({
417
414
  {
418
415
  "aria-hidden": "true",
419
416
  role: "presentation",
420
- className: joinClassNames(
421
- "arc-tabs__active-indicator",
422
- indicator.ready && "is-ready"
423
- ),
417
+ className: joinClassNames("arc-tabs__active-indicator", indicator.ready && "is-ready"),
424
418
  style: indicatorStyle
425
419
  }
426
420
  ) : null,
@@ -445,9 +439,15 @@ function ArcTabs({
445
439
  tabIndex: tabIndexValue,
446
440
  disabled,
447
441
  className: "arc-tabs__tab",
448
- onFocus: () => setFocusedIndex(index),
449
- onClick: () => selectTab(index),
450
- onKeyDown: (event) => handleTabKeyDown(event, index),
442
+ onFocus: () => {
443
+ setFocusedIndex(index);
444
+ },
445
+ onClick: () => {
446
+ selectTab(index);
447
+ },
448
+ onKeyDown: (event) => {
449
+ handleTabKeyDown(event, index);
450
+ },
451
451
  children: renderTabLabel ? renderTabLabel(item, state) : renderDefaultLabel(item)
452
452
  }
453
453
  ) }, item.id);
@@ -519,7 +519,7 @@ var getNextEnabledIndex2 = (enabledIndices, currentIndex, direction) => {
519
519
  if (!enabledIndices.length) return -1;
520
520
  const currentPosition = enabledIndices.indexOf(currentIndex);
521
521
  if (currentPosition === -1) {
522
- return direction === 1 ? enabledIndices[0] ?? -1 : enabledIndices[enabledIndices.length - 1] ?? -1;
522
+ return direction === 1 ? enabledIndices[0] ?? -1 : enabledIndices.at(-1) ?? -1;
523
523
  }
524
524
  const nextPosition = (currentPosition + direction + enabledIndices.length) % enabledIndices.length;
525
525
  return enabledIndices[nextPosition] ?? -1;
@@ -566,15 +566,10 @@ function ArcTabsTailwind({
566
566
  [listId, reactId]
567
567
  );
568
568
  const isControlled = value !== void 0;
569
- const firstEnabledIndex = React2.useMemo(
570
- () => findFirstEnabledIndex2(items),
571
- [items]
572
- );
569
+ const firstEnabledIndex = React2.useMemo(() => findFirstEnabledIndex2(items), [items]);
573
570
  const [uncontrolledValue, setUncontrolledValue] = React2.useState(() => {
574
571
  const requested = defaultValue;
575
- const requestedMatch = items.find(
576
- (item) => !item.disabled && item.id === requested
577
- );
572
+ const requestedMatch = items.find((item) => !item.disabled && item.id === requested);
578
573
  if (requestedMatch) return requestedMatch.id;
579
574
  return firstEnabledIndex >= 0 ? items[firstEnabledIndex]?.id : void 0;
580
575
  });
@@ -612,7 +607,9 @@ function ArcTabsTailwind({
612
607
  const hasMountedRef = React2.useRef(false);
613
608
  const previousSelectedIndexRef = React2.useRef(selectedIndex);
614
609
  const [hasInteracted, setHasInteracted] = React2.useState(false);
615
- const [panelDirection, setPanelDirection] = React2.useState("none");
610
+ const [panelDirection, setPanelDirection] = React2.useState(
611
+ "none"
612
+ );
616
613
  const [indicator, setIndicator] = React2.useState({
617
614
  x: 0,
618
615
  width: 0,
@@ -697,12 +694,18 @@ function ArcTabsTailwind({
697
694
  if (!showSlidingIndicator) return;
698
695
  const listElement = listRef.current;
699
696
  if (!listElement) return;
700
- const onResize = () => syncIndicator();
701
- const onScroll = () => syncIndicator();
697
+ const onResize = () => {
698
+ syncIndicator();
699
+ };
700
+ const onScroll = () => {
701
+ syncIndicator();
702
+ };
702
703
  const frame = requestAnimationFrame(syncIndicator);
703
704
  let observer = null;
704
705
  if (typeof ResizeObserver !== "undefined") {
705
- observer = new ResizeObserver(() => syncIndicator());
706
+ observer = new ResizeObserver(() => {
707
+ syncIndicator();
708
+ });
706
709
  observer.observe(listElement);
707
710
  tabRefs.current.forEach((tabElement) => {
708
711
  if (tabElement) observer?.observe(tabElement);
@@ -725,7 +728,7 @@ function ArcTabsTailwind({
725
728
  if (!panelElement || typeof panelElement.animate !== "function") {
726
729
  return;
727
730
  }
728
- if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
731
+ if (typeof globalThis.window !== "undefined" && globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
729
732
  return;
730
733
  }
731
734
  const offsetX = motionPreset === "expressive" ? panelDirection === "forward" ? 20 : panelDirection === "backward" ? -20 : 0 : 0;
@@ -753,13 +756,7 @@ function ArcTabsTailwind({
753
756
  return () => {
754
757
  animation.cancel();
755
758
  };
756
- }, [
757
- selectedIndex,
758
- hasInteracted,
759
- motionPreset,
760
- panelDirection,
761
- effectiveMotionDuration
762
- ]);
759
+ }, [selectedIndex, hasInteracted, motionPreset, panelDirection, effectiveMotionDuration]);
763
760
  const handleTabKeyDown = React2.useCallback(
764
761
  (event, index) => {
765
762
  if (!enabledIndices.length) return;
@@ -793,7 +790,7 @@ function ArcTabsTailwind({
793
790
  }
794
791
  case "End": {
795
792
  event.preventDefault();
796
- const last = enabledIndices[enabledIndices.length - 1];
793
+ const last = enabledIndices.at(-1);
797
794
  if (last !== void 0) {
798
795
  focusTabIndex(last);
799
796
  if (activationMode === "automatic") selectTab(last);
@@ -867,7 +864,7 @@ function ArcTabsTailwind({
867
864
  tabsClassName
868
865
  );
869
866
  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)]",
867
+ "relative z-[2] mt-0 rounded-b-[var(--arc-radius)] rounded-t-none border border-t-0 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
868
  classNames?.panels,
872
869
  panelClassName
873
870
  );
@@ -958,9 +955,15 @@ function ArcTabsTailwind({
958
955
  tabIndex: tabIndexValue,
959
956
  disabled,
960
957
  className: tabClassName,
961
- onFocus: () => setFocusedIndex(index),
962
- onClick: () => selectTab(index),
963
- onKeyDown: (event) => handleTabKeyDown(event, index),
958
+ onFocus: () => {
959
+ setFocusedIndex(index);
960
+ },
961
+ onClick: () => {
962
+ selectTab(index);
963
+ },
964
+ onKeyDown: (event) => {
965
+ handleTabKeyDown(event, index);
966
+ },
964
967
  "data-slot": "tab",
965
968
  children: renderTabLabel ? renderTabLabel(item, state) : renderDefaultLabel(item)
966
969
  }