@ehfuse/overlay-scrollbar 1.0.0 → 1.2.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.esm.js CHANGED
@@ -1,52 +1,26 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { forwardRef, useRef, useEffect, useState, useImperativeHandle, useCallback } from 'react';
2
+ import { forwardRef, useRef, useState, useImperativeHandle, useCallback, useEffect, useMemo } from 'react';
3
3
 
4
- const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, ref) => {
4
+ const OverlayScrollbar = forwardRef(({ className = "", style = {}, children, onScroll, scrollbarWidth = 8, // deprecated
5
+ thumbRadius, showArrows = false, arrowStep = 50, trackWidth = 16, thumbWidth = 8, thumbMinHeight = 50, trackColor = "rgba(128, 128, 128, 0.1)", thumbColor = "rgba(128, 128, 128, 0.6)", thumbActiveColor = "rgba(128, 128, 128, 0.9)", arrowColor = "rgba(128, 128, 128, 0.6)", arrowActiveColor = "rgba(64, 64, 64, 1.0)", hideDelay = 1500, hideDelayOnWheel = 700, }, ref) => {
5
6
  const containerRef = useRef(null);
6
7
  const contentRef = useRef(null);
7
8
  const scrollbarRef = useRef(null);
8
9
  const thumbRef = useRef(null);
9
- // 웹킷 스크롤바 숨김을 위한 스타일
10
- useEffect(() => {
11
- const style = document.createElement("style");
12
- style.textContent = `
13
- .overlay-scrollbar-container::-webkit-scrollbar {
14
- display: none !important;
15
- width: 0 !important;
16
- height: 0 !important;
17
- background: transparent !important;
18
- }
19
- .overlay-scrollbar-container::-webkit-scrollbar-track {
20
- display: none !important;
21
- }
22
- .overlay-scrollbar-container::-webkit-scrollbar-thumb {
23
- display: none !important;
24
- }
25
- .overlay-scrollbar-container::-webkit-scrollbar-corner {
26
- display: none !important;
27
- }
28
- .overlay-scrollbar-container * {
29
- scrollbar-width: none;
30
- -ms-overflow-style: none;
31
- }
32
- .overlay-scrollbar-container *::-webkit-scrollbar {
33
- display: none !important;
34
- width: 0 !important;
35
- height: 0 !important;
36
- }
37
- `;
38
- document.head.appendChild(style);
39
- return () => {
40
- document.head.removeChild(style);
41
- };
42
- }, []);
10
+ // 기본 상태들
43
11
  const [scrollbarVisible, setScrollbarVisible] = useState(false);
44
- const [trackVisible, setTrackVisible] = useState(false); // 트랙 표시 상태 추가
45
12
  const [isDragging, setIsDragging] = useState(false);
46
13
  const [dragStart, setDragStart] = useState({ y: 0, scrollTop: 0 });
47
14
  const [thumbHeight, setThumbHeight] = useState(0);
48
15
  const [thumbTop, setThumbTop] = useState(0);
49
- // 스크롤바 표시/숨김 타이머
16
+ const [activeArrow, setActiveArrow] = useState(null);
17
+ const [hoveredArrow, setHoveredArrow] = useState(null);
18
+ // 초기 마운트 시 hover 방지용
19
+ const [isInitialized, setIsInitialized] = useState(false);
20
+ // 휠 스크롤 감지용
21
+ const wheelTimeoutRef = useRef(null);
22
+ const [isWheelScrolling, setIsWheelScrolling] = useState(false);
23
+ // 숨김 타이머
50
24
  const hideTimeoutRef = useRef(null);
51
25
  // ref를 통해 외부에서 스크롤 컨테이너에 접근할 수 있도록 함
52
26
  useImperativeHandle(ref, () => ({
@@ -69,72 +43,60 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
69
43
  return ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
70
44
  },
71
45
  }), []);
72
- // 스크롤바 숨김 타이머 취소
46
+ // 스크롤 가능 여부 체크
47
+ const isScrollable = useCallback(() => {
48
+ if (!containerRef.current || !contentRef.current)
49
+ return false;
50
+ return (contentRef.current.scrollHeight >
51
+ containerRef.current.clientHeight + 2);
52
+ }, []);
53
+ // 타이머 정리
73
54
  const clearHideTimer = useCallback(() => {
74
55
  if (hideTimeoutRef.current) {
75
56
  clearTimeout(hideTimeoutRef.current);
76
57
  hideTimeoutRef.current = null;
77
58
  }
78
59
  }, []);
79
- // 스크롤바 숨김 타이머 설정
60
+ // 스크롤바 숨기기 타이머
80
61
  const setHideTimer = useCallback((delay) => {
81
- clearHideTimer(); // 기존 타이머 취소
62
+ clearHideTimer();
82
63
  hideTimeoutRef.current = setTimeout(() => {
83
- if (!isDragging) {
84
- setScrollbarVisible(false);
85
- setTrackVisible(false);
86
- }
64
+ setScrollbarVisible(false);
87
65
  hideTimeoutRef.current = null;
88
66
  }, delay);
89
- }, [isDragging, clearHideTimer]);
90
- // 스크롤 가능 여부 체크 함수
91
- const isScrollable = useCallback(() => {
92
- if (!containerRef.current || !contentRef.current)
93
- return false;
94
- const container = containerRef.current;
95
- const content = contentRef.current;
96
- return content.scrollHeight > container.clientHeight + 2;
97
- }, []);
98
- // 스크롤바 크기 및 위치 계산
67
+ }, [clearHideTimer, isDragging]);
68
+ // 스크롤바 위치 크기 업데이트
99
69
  const updateScrollbar = useCallback(() => {
100
- if (!containerRef.current || !contentRef.current)
70
+ if (!containerRef.current ||
71
+ !contentRef.current ||
72
+ !scrollbarRef.current)
101
73
  return;
102
74
  const container = containerRef.current;
103
75
  const content = contentRef.current;
104
76
  const containerHeight = container.clientHeight;
105
77
  const contentHeight = content.scrollHeight;
106
78
  const scrollTop = container.scrollTop;
107
- // console.log("스크롤바 업데이트:", {
108
- // containerHeight,
109
- // contentHeight,
110
- // scrollTop,
111
- // hasScrollableContent: contentHeight > containerHeight,
112
- // });
113
- // 스크롤 가능한 콘텐츠가 있는지 확인 (여유분 2px 추가로 더 정확한 판단)
79
+ // 스크롤 불가능하면 숨김
114
80
  if (contentHeight <= containerHeight + 2) {
115
- // console.log("스크롤 불가능한 콘텐츠, 스크롤바 숨김");
116
81
  setScrollbarVisible(false);
117
- setTrackVisible(false);
118
- clearHideTimer(); // 타이머도 정리
82
+ clearHideTimer();
119
83
  return;
120
84
  }
121
- // 높이 계산 (최소 20px, 최대 컨테이너의 90%)
122
- const thumbHeightRatio = containerHeight / contentHeight;
123
- const calculatedThumbHeight = Math.max(20, Math.min(containerHeight * 0.9, containerHeight * thumbHeightRatio));
124
- // 위치 계산
125
- const scrollRatio = scrollTop / (contentHeight - containerHeight);
126
- const maxThumbTop = containerHeight - calculatedThumbHeight;
127
- const calculatedThumbTop = scrollRatio * maxThumbTop;
128
- // console.log("썸 계산:", {
129
- // thumbHeightRatio,
130
- // calculatedThumbHeight,
131
- // scrollRatio,
132
- // calculatedThumbTop,
133
- // maxThumbTop,
134
- // });
85
+ // 화살표와 간격 공간 계산 (화살표 + 위아래여백 4px + 화살표간격 4px씩, 화살표 없어도 위아래 4px씩 여백)
86
+ const arrowSpace = showArrows ? scrollbarWidth * 2 + 16 : 8;
87
+ // 높이 계산 (사용자 설정 최소 높이 사용, 화살표 공간 제외)
88
+ const availableHeight = containerHeight - arrowSpace;
89
+ const scrollRatio = containerHeight / contentHeight;
90
+ const calculatedThumbHeight = Math.max(availableHeight * scrollRatio, thumbMinHeight);
91
+ // 위치 계산 (화살표와 간격 공간 제외)
92
+ const scrollableHeight = contentHeight - containerHeight;
93
+ const thumbScrollableHeight = availableHeight - calculatedThumbHeight;
94
+ const calculatedThumbTop = scrollableHeight > 0
95
+ ? (scrollTop / scrollableHeight) * thumbScrollableHeight
96
+ : 0;
135
97
  setThumbHeight(calculatedThumbHeight);
136
98
  setThumbTop(calculatedThumbTop);
137
- }, []);
99
+ }, [clearHideTimer, showArrows, scrollbarWidth, thumbMinHeight]);
138
100
  // 썸 드래그 시작
139
101
  const handleThumbMouseDown = useCallback((event) => {
140
102
  event.preventDefault();
@@ -146,24 +108,21 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
146
108
  y: event.clientY,
147
109
  scrollTop: containerRef.current.scrollTop,
148
110
  });
111
+ clearHideTimer();
149
112
  setScrollbarVisible(true);
150
- setTrackVisible(true); // 드래그 시 트랙 표시
151
- clearHideTimer(); // 드래그 중에는 타이머 취소
152
113
  }, [clearHideTimer]);
153
114
  // 썸 드래그 중
154
115
  const handleMouseMove = useCallback((event) => {
155
116
  if (!isDragging || !containerRef.current || !contentRef.current)
156
117
  return;
157
- event.preventDefault();
158
118
  const container = containerRef.current;
159
119
  const content = contentRef.current;
160
120
  const containerHeight = container.clientHeight;
161
121
  const contentHeight = content.scrollHeight;
162
- const deltaY = event.clientY - dragStart.y;
163
122
  const scrollableHeight = contentHeight - containerHeight;
164
- const maxThumbTop = containerHeight - thumbHeight;
165
- // 드래그 거리를 스크롤 거리로 변환
166
- const scrollDelta = (deltaY / maxThumbTop) * scrollableHeight;
123
+ const deltaY = event.clientY - dragStart.y;
124
+ const thumbScrollableHeight = containerHeight - thumbHeight;
125
+ const scrollDelta = (deltaY / thumbScrollableHeight) * scrollableHeight;
167
126
  const newScrollTop = Math.max(0, Math.min(scrollableHeight, dragStart.scrollTop + scrollDelta));
168
127
  container.scrollTop = newScrollTop;
169
128
  updateScrollbar();
@@ -171,10 +130,11 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
171
130
  // 썸 드래그 종료
172
131
  const handleMouseUp = useCallback(() => {
173
132
  setIsDragging(false);
174
- setTrackVisible(false); // 드래그 종료 시 트랙 숨김
175
- setHideTimer(2000); // 2초 숨김
176
- }, [setHideTimer]);
177
- // 스크롤바 트랙 클릭
133
+ if (isScrollable()) {
134
+ setHideTimer(hideDelay); // 기본 숨김 시간 적용
135
+ }
136
+ }, [isScrollable, setHideTimer, hideDelay]);
137
+ // 트랙 클릭으로 스크롤 점프
178
138
  const handleTrackClick = useCallback((event) => {
179
139
  if (!containerRef.current ||
180
140
  !contentRef.current ||
@@ -192,43 +152,89 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
192
152
  container.scrollTop = Math.max(0, Math.min(contentHeight - containerHeight, newScrollTop));
193
153
  updateScrollbar();
194
154
  setScrollbarVisible(true);
195
- setTrackVisible(true); // 클릭 시 트랙 표시
196
- setHideTimer(2000); // 클릭 후 2초간 유지
197
- }, [updateScrollbar, setHideTimer]);
198
- // 이벤트 리스너 등록
155
+ setHideTimer(hideDelay);
156
+ }, [updateScrollbar, setHideTimer, hideDelay]);
157
+ // 위쪽 화살표 클릭 핸들러
158
+ const handleUpArrowClick = useCallback((event) => {
159
+ event.preventDefault();
160
+ event.stopPropagation();
161
+ if (!containerRef.current)
162
+ return;
163
+ const newScrollTop = Math.max(0, containerRef.current.scrollTop - arrowStep);
164
+ containerRef.current.scrollTop = newScrollTop;
165
+ updateScrollbar();
166
+ setScrollbarVisible(true);
167
+ setHideTimer(hideDelay);
168
+ }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
169
+ // 아래쪽 화살표 클릭 핸들러
170
+ const handleDownArrowClick = useCallback((event) => {
171
+ event.preventDefault();
172
+ event.stopPropagation();
173
+ if (!containerRef.current || !contentRef.current)
174
+ return;
175
+ const container = containerRef.current;
176
+ const content = contentRef.current;
177
+ const maxScrollTop = content.scrollHeight - container.clientHeight;
178
+ const newScrollTop = Math.min(maxScrollTop, container.scrollTop + arrowStep);
179
+ container.scrollTop = newScrollTop;
180
+ updateScrollbar();
181
+ setScrollbarVisible(true);
182
+ setHideTimer(hideDelay);
183
+ }, [updateScrollbar, setHideTimer, arrowStep, hideDelay]);
184
+ // 스크롤 이벤트 리스너
199
185
  useEffect(() => {
200
186
  const container = containerRef.current;
201
187
  if (!container)
202
188
  return;
203
- // 이벤트 핸들러 (마우스 스크롤 감지)
204
- const handleWheel = () => {
205
- clearHideTimer(); // 먼저 기존 타이머 취소
206
- setScrollbarVisible(true);
207
- // 휠 스크롤 시에는 트랙 숨김 (thumb만 표시)
189
+ const handleScroll = (event) => {
208
190
  updateScrollbar();
209
- setHideTimer(700); // 0.7초 숨김
210
- };
211
- // 스크롤 이벤트 디바운스
212
- const debouncedScroll = (event) => {
213
- clearHideTimer(); // 먼저 기존 타이머 취소
191
+ // 스크롤 중에는 스크롤바 표시
192
+ clearHideTimer();
214
193
  setScrollbarVisible(true);
215
- // 스크롤 시에도 트랙 숨김 (thumb만 표시)
216
- updateScrollbar();
217
- setHideTimer(700); // 0.7초 후 숨김
194
+ // 스크롤 중이면 빠른 숨김, 아니면 기본 숨김 시간 적용
195
+ const delay = isWheelScrolling ? hideDelayOnWheel : hideDelay;
196
+ setHideTimer(delay);
218
197
  if (onScroll) {
219
198
  onScroll(event);
220
199
  }
221
200
  };
222
- container.addEventListener("scroll", debouncedScroll, {
201
+ const handleWheel = () => {
202
+ // 휠 스크롤 상태 표시
203
+ setIsWheelScrolling(true);
204
+ // 기존 휠 타이머 제거
205
+ if (wheelTimeoutRef.current) {
206
+ clearTimeout(wheelTimeoutRef.current);
207
+ }
208
+ // 300ms 후 휠 스크롤 상태 해제 (휠 스크롤이 끝났다고 간주)
209
+ wheelTimeoutRef.current = setTimeout(() => {
210
+ setIsWheelScrolling(false);
211
+ }, 300);
212
+ clearHideTimer();
213
+ setScrollbarVisible(true);
214
+ };
215
+ container.addEventListener("scroll", handleScroll, {
216
+ passive: true,
217
+ });
218
+ container.addEventListener("wheel", handleWheel, {
223
219
  passive: true,
224
220
  });
225
- container.addEventListener("wheel", handleWheel, { passive: true });
226
221
  return () => {
227
- container.removeEventListener("scroll", debouncedScroll);
222
+ container.removeEventListener("scroll", handleScroll);
228
223
  container.removeEventListener("wheel", handleWheel);
224
+ if (wheelTimeoutRef.current) {
225
+ clearTimeout(wheelTimeoutRef.current);
226
+ }
229
227
  };
230
- }, [updateScrollbar, isDragging, onScroll, clearHideTimer, setHideTimer]);
231
- // 마우스 이벤트 리스너 등록 (드래그)
228
+ }, [
229
+ updateScrollbar,
230
+ onScroll,
231
+ clearHideTimer,
232
+ setHideTimer,
233
+ hideDelay,
234
+ hideDelayOnWheel,
235
+ isWheelScrolling,
236
+ ]);
237
+ // 전역 마우스 이벤트 리스너
232
238
  useEffect(() => {
233
239
  if (isDragging) {
234
240
  document.addEventListener("mousemove", handleMouseMove);
@@ -239,153 +245,172 @@ const OverlayScrollbar = forwardRef(({ children, onScroll, className, style }, r
239
245
  };
240
246
  }
241
247
  }, [isDragging, handleMouseMove, handleMouseUp]);
242
- // 초기 스크롤바 계산 및 스크롤 감지
248
+ // 초기 스크롤바 업데이트
243
249
  useEffect(() => {
244
- const checkAndUpdateScrollbar = () => {
245
- updateScrollbar();
246
- // 초기에 스크롤 가능한 콘텐츠가 있는지 확인
247
- const container = containerRef.current;
248
- const content = contentRef.current;
249
- if (container && content) {
250
- const hasScrollableContent = content.scrollHeight > container.clientHeight + 2; // 여유분 2px 추가
251
- // console.log("초기 스크롤바 체크:", {
252
- // hasScrollableContent,
253
- // contentScrollHeight: content.scrollHeight,
254
- // containerClientHeight: container.clientHeight,
255
- // });
256
- if (hasScrollableContent) {
257
- // 초기에는 스크롤바를 숨김 상태로 유지 (스크롤이나 hover 시에만 표시)
258
- setScrollbarVisible(false);
259
- setTrackVisible(false);
260
- }
261
- else {
262
- // 스크롤이 필요 없으면 확실히 숨김
263
- setScrollbarVisible(false);
264
- setTrackVisible(false);
265
- }
266
- }
267
- };
268
- // 차트 렌더링을 고려하여 더 긴 지연시간 적용
269
- const timeoutId = setTimeout(checkAndUpdateScrollbar, 200);
270
- return () => clearTimeout(timeoutId);
271
- }, [updateScrollbar, children, isDragging]);
272
- // 리사이즈 옵저버
250
+ updateScrollbar();
251
+ }, [updateScrollbar]);
252
+ // 컴포넌트 초기화 완료 표시 (hover 이벤트 활성화용)
273
253
  useEffect(() => {
274
- const container = containerRef.current;
275
- const content = contentRef.current;
276
- if (!container || !content)
254
+ const timer = setTimeout(() => {
255
+ setIsInitialized(true);
256
+ }, 100); // 100ms 후 초기화 완료
257
+ return () => clearTimeout(timer);
258
+ }, []);
259
+ // Resize observer로 크기 변경 감지
260
+ useEffect(() => {
261
+ if (!containerRef.current || !contentRef.current)
277
262
  return;
278
263
  const resizeObserver = new ResizeObserver(() => {
279
- // 차트 렌더링 지연을 고려하여 디바운스 적용
280
- setTimeout(() => {
281
- updateScrollbar();
282
- }, 100);
264
+ updateScrollbar();
283
265
  });
284
- resizeObserver.observe(container);
285
- resizeObserver.observe(content);
286
- return () => {
287
- resizeObserver.disconnect();
288
- };
266
+ resizeObserver.observe(containerRef.current);
267
+ resizeObserver.observe(contentRef.current);
268
+ return () => resizeObserver.disconnect();
289
269
  }, [updateScrollbar]);
290
- // 컴포넌트 언마운트 타이머 정리
270
+ // 계산된 값들을 메모이제이션하여 안정화
271
+ const { finalThumbWidth, finalTrackWidth } = useMemo(() => {
272
+ const computedThumbWidth = thumbWidth !== undefined ? thumbWidth : scrollbarWidth;
273
+ let computedTrackWidth = trackWidth !== undefined ? trackWidth : scrollbarWidth * 2;
274
+ // thumbWidth가 trackWidth보다 크거나 같으면 trackWidth를 thumbWidth와 같게 설정
275
+ if (computedThumbWidth >= computedTrackWidth) {
276
+ computedTrackWidth = computedThumbWidth;
277
+ }
278
+ return {
279
+ finalThumbWidth: computedThumbWidth,
280
+ finalTrackWidth: computedTrackWidth,
281
+ };
282
+ }, [thumbWidth, trackWidth, scrollbarWidth]);
283
+ // 썸 radius 계산 (기본값: thumbWidth / 2)
284
+ const calculatedThumbRadius = thumbRadius !== undefined ? thumbRadius : finalThumbWidth / 2;
285
+ // 화살표 색상 계산 (기본값: 독립적인 색상)
286
+ const finalArrowColor = arrowColor || "rgba(128, 128, 128, 0.8)";
287
+ const finalArrowActiveColor = arrowActiveColor || "rgba(128, 128, 128, 1.0)";
288
+ // 웹킷 스크롤바 숨기기용 CSS 동적 주입
291
289
  useEffect(() => {
290
+ const styleId = "overlay-scrollbar-webkit-hide";
291
+ // 이미 스타일이 있으면 제거
292
+ const existingStyle = document.getElementById(styleId);
293
+ if (existingStyle) {
294
+ existingStyle.remove();
295
+ }
296
+ const style = document.createElement("style");
297
+ style.id = styleId;
298
+ style.textContent = `
299
+ .overlay-scrollbar-container::-webkit-scrollbar {
300
+ display: none !important;
301
+ width: 0 !important;
302
+ height: 0 !important;
303
+ }
304
+ .overlay-scrollbar-container::-webkit-scrollbar-track {
305
+ display: none !important;
306
+ }
307
+ .overlay-scrollbar-container::-webkit-scrollbar-thumb {
308
+ display: none !important;
309
+ }
310
+ `;
311
+ document.head.appendChild(style);
292
312
  return () => {
293
- if (hideTimeoutRef.current) {
294
- clearTimeout(hideTimeoutRef.current);
313
+ const styleToRemove = document.getElementById(styleId);
314
+ if (styleToRemove) {
315
+ styleToRemove.remove();
295
316
  }
296
317
  };
297
318
  }, []);
298
- return (jsxs("div", { className: `overlay-scrollbar ${className || ""}`, style: Object.assign({ position: "relative", overflow: "hidden", display: "flex", flexGrow: 1, width: "100%" }, style), children: [jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", style: {
299
- display: "flex",
300
- flexGrow: 1,
301
- overflow: "auto",
319
+ return (jsxs("div", { className: `overlay-scrollbar-wrapper ${className}`, style: Object.assign({ display: "flex", flexDirection: "column", position: "relative", minHeight: 0, height: "100%", flex: "1 1 0%" }, style), children: [jsx("div", { ref: containerRef, className: "overlay-scrollbar-container", style: {
320
+ width: "100%", // 명시적 너비 설정
321
+ height: "100%", // 상위 컨테이너의 전체 높이 사용
322
+ minHeight: 0, // 최소 높이 보장
323
+ overflow: "auto", // 네이티브 스크롤 기능 유지
324
+ // 브라우저 기본 스크롤바만 숨기기
302
325
  scrollbarWidth: "none", // Firefox
303
326
  msOverflowStyle: "none", // IE/Edge
304
- }, children: jsx("div", { ref: contentRef, style: { height: "100%", width: "100%" }, children: children }) }), jsx("div", { style: {
305
- position: "absolute",
306
- top: 0,
307
- right: 0,
308
- width: 20, // 20px 넓은 hover 영역
309
- height: "100%",
310
- zIndex: 5,
311
- pointerEvents: "auto",
312
- }, onMouseEnter: () => {
313
- // 스크롤 가능한 경우에만 스크롤바 표시
327
+ }, children: jsx("div", { ref: contentRef, className: "overlay-scrollbar-content", style: {
328
+ minHeight: "100%",
329
+ }, children: children }) }), jsxs("div", { ref: scrollbarRef, className: "overlay-scrollbar-track", onMouseEnter: () => {
314
330
  if (isScrollable()) {
315
331
  clearHideTimer();
316
332
  setScrollbarVisible(true);
317
- setTrackVisible(true); // hover 시 트랙까지 표시
318
333
  }
319
334
  }, onMouseLeave: () => {
320
- // 스크롤바 hover 영역에서 벗어남 시
321
335
  if (!isDragging && isScrollable()) {
322
- setTrackVisible(false); // 트랙 숨김
323
- setHideTimer(1000); // 1초 후 숨김
336
+ setHideTimer(hideDelay);
324
337
  }
325
- } }), jsx("div", { ref: scrollbarRef, onClick: handleTrackClick, onMouseEnter: () => {
326
- // 스크롤 가능한 경우에만 스크롤바 영역에 hover 시 타이머 취소하고 표시 유지
327
- if (isScrollable()) {
328
- clearHideTimer();
329
- setScrollbarVisible(true);
330
- setTrackVisible(true);
331
- }
332
- }, onMouseLeave: () => {
333
- // 스크롤바 영역에서 벗어나면 일정 시간 후 숨김
334
- if (!isDragging && isScrollable()) {
335
- setHideTimer(1000);
336
- }
337
- }, className: `overlay-scrollbar-track ${scrollbarVisible ? "visible" : ""}`, style: {
338
+ }, style: {
338
339
  position: "absolute",
339
340
  top: 0,
340
- right: 2,
341
- width: 8,
341
+ right: 0, // 완전히 오른쪽에 붙임
342
+ width: `${finalTrackWidth}px`, // hover 영역 너비
342
343
  height: "100%",
343
344
  opacity: scrollbarVisible ? 1 : 0,
344
- transition: "opacity 0.3s ease-in-out",
345
- pointerEvents: scrollbarVisible ? "auto" : "none",
346
- zIndex: 10,
345
+ transition: "opacity 0.2s ease-in-out",
347
346
  cursor: "pointer",
348
- borderRadius: "4px",
349
- // trackVisible 상태에 따라 트랙 배경 표시
350
- backgroundColor: trackVisible
351
- ? "rgba(200, 200, 200, 0.3)"
352
- : "transparent",
353
- }, onMouseOver: (e) => {
354
- e.target.style.backgroundColor =
355
- "rgba(0, 0, 0, 0.1)";
356
- }, onMouseOut: (e) => {
357
- e.target.style.backgroundColor =
358
- trackVisible
359
- ? "rgba(200, 200, 200, 0.3)"
360
- : "transparent";
361
- }, children: jsx("div", { ref: thumbRef, onMouseDown: handleThumbMouseDown, style: {
362
- position: "absolute",
363
- top: `${thumbTop}px`,
364
- left: 0,
365
- width: "100%",
366
- height: `${Math.max(thumbHeight, 30)}px`, // 최소 30px 보장
367
- backgroundColor: isDragging
368
- ? "rgba(0, 0, 0, 0.7)"
369
- : "rgba(0, 0, 0, 0.5)",
370
- borderRadius: "4px",
371
- cursor: "pointer",
372
- minHeight: "30px", // CSS로도 최소 높이 보장
373
- transition: isDragging
374
- ? "none"
375
- : "background-color 0.2s ease, transform 0.1s ease",
376
- transform: isDragging ? "scaleX(1.2)" : "scaleX(1)",
377
- opacity: isDragging ? 1 : 0.4,
378
- }, onMouseOver: (e) => {
379
- e.target.style.opacity = "1";
380
- e.target.style.transform =
381
- "scaleX(1.1)";
382
- }, onMouseOut: (e) => {
383
- if (!isDragging) {
384
- e.target.style.opacity = "0.4";
385
- e.target.style.transform =
386
- "scaleX(1)";
387
- }
388
- } }) })] }));
347
+ zIndex: 1000,
348
+ pointerEvents: "auto", // 항상 이벤트 활성화 (hover 감지용)
349
+ }, children: [jsx("div", { className: "overlay-scrollbar-track-background", onClick: handleTrackClick, style: {
350
+ position: "absolute",
351
+ top: showArrows
352
+ ? `${finalThumbWidth + 8}px`
353
+ : "4px",
354
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
355
+ width: `${finalThumbWidth}px`,
356
+ height: showArrows
357
+ ? `calc(100% - ${finalThumbWidth * 2 + 16}px)`
358
+ : "calc(100% - 8px)",
359
+ backgroundColor: trackColor,
360
+ borderRadius: `${calculatedThumbRadius}px`,
361
+ cursor: "pointer",
362
+ } }), jsx("div", { ref: thumbRef, className: "overlay-scrollbar-thumb", onMouseDown: handleThumbMouseDown, style: {
363
+ position: "absolute",
364
+ top: `${(showArrows ? finalThumbWidth + 8 : 4) +
365
+ thumbTop}px`,
366
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
367
+ width: `${finalThumbWidth}px`,
368
+ height: `${Math.max(thumbHeight, thumbMinHeight)}px`,
369
+ backgroundColor: isDragging
370
+ ? thumbActiveColor
371
+ : thumbColor,
372
+ borderRadius: `${calculatedThumbRadius}px`,
373
+ cursor: "pointer",
374
+ transition: isDragging
375
+ ? "none"
376
+ : "background-color 0.2s ease-in-out",
377
+ } })] }), showArrows && (jsx("div", { className: "overlay-scrollbar-up-arrow", onClick: handleUpArrowClick, onMouseEnter: () => setHoveredArrow("up"), onMouseLeave: () => setHoveredArrow(null), style: {
378
+ position: "absolute",
379
+ top: "4px",
380
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
381
+ width: `${finalThumbWidth}px`,
382
+ height: `${finalThumbWidth}px`,
383
+ cursor: "pointer",
384
+ display: "flex",
385
+ alignItems: "center",
386
+ justifyContent: "center",
387
+ fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
388
+ color: hoveredArrow === "up"
389
+ ? finalArrowActiveColor
390
+ : finalArrowColor,
391
+ userSelect: "none",
392
+ zIndex: 1001,
393
+ opacity: scrollbarVisible ? 1 : 0,
394
+ transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
395
+ }, children: "\u25B2" })), showArrows && (jsx("div", { className: "overlay-scrollbar-down-arrow", onClick: handleDownArrowClick, onMouseEnter: () => setHoveredArrow("down"), onMouseLeave: () => setHoveredArrow(null), style: {
396
+ position: "absolute",
397
+ bottom: "4px",
398
+ right: `${(finalTrackWidth - finalThumbWidth) / 2}px`, // 트랙 가운데 정렬
399
+ width: `${finalThumbWidth}px`,
400
+ height: `${finalThumbWidth}px`,
401
+ cursor: "pointer",
402
+ display: "flex",
403
+ alignItems: "center",
404
+ justifyContent: "center",
405
+ fontSize: `${Math.max(finalThumbWidth * 0.75, 8)}px`,
406
+ color: hoveredArrow === "down"
407
+ ? finalArrowActiveColor
408
+ : finalArrowColor,
409
+ userSelect: "none",
410
+ zIndex: 1001,
411
+ opacity: scrollbarVisible ? 1 : 0,
412
+ transition: "opacity 0.2s ease-in-out, color 0.15s ease-in-out",
413
+ }, children: "\u25BC" }))] }));
389
414
  });
390
415
 
391
416
  export { OverlayScrollbar, OverlayScrollbar as default };