66 AnimatePresence ,
77 useMotionValue ,
88 useTransform ,
9- MotionProps
9+ MotionProps ,
10+ PanInfo
1011} from 'framer-motion' ;
1112
1213interface BottomSheetProps extends MotionProps {
@@ -32,20 +33,43 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
3233 const [ windowHeight , setWindowHeight ] = useState < number > ( 0 ) ;
3334
3435 useEffect ( ( ) => {
35- setWindowHeight ( window . innerHeight ) ;
36+ const updateHeight = ( ) => {
37+ setWindowHeight ( window . innerHeight ) ;
38+ } ;
39+
40+ updateHeight ( ) ;
41+ window . addEventListener ( 'resize' , updateHeight ) ;
42+ window . addEventListener ( 'orientationchange' , updateHeight ) ;
43+
44+ return ( ) => {
45+ window . removeEventListener ( 'resize' , updateHeight ) ;
46+ window . removeEventListener ( 'orientationchange' , updateHeight ) ;
47+ } ;
3648 } , [ ] ) ;
3749
3850 const initialHeightValue = windowHeight * 0.2 ;
3951 const y = useMotionValue ( initialHeightValue ) ;
4052 const bottomSheetRef = useRef < HTMLDivElement > ( null ) ;
53+ const dragHandleRef = useRef < HTMLDivElement > ( null ) ;
4154
4255 const handleDragEnd = (
4356 event : MouseEvent | TouchEvent | PointerEvent ,
44- info : { velocity : { y : number } }
57+ info : PanInfo
4558 ) => {
4659 const threshold = windowHeight * 0.25 ;
4760 const closeThreshold = windowHeight * 0.7 ;
4861 const endY = y . get ( ) ;
62+ const velocity = info . velocity . y ;
63+
64+ if ( velocity > 500 ) {
65+ setIsVisible ( false ) ;
66+ return ;
67+ }
68+
69+ if ( velocity < - 500 ) {
70+ y . set ( 0 ) ;
71+ return ;
72+ }
4973
5074 if ( endY < threshold ) {
5175 y . set ( 0 ) ;
@@ -67,6 +91,35 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
6791 }
6892 } ;
6993
94+ useEffect ( ( ) => {
95+ if ( ! isVisible ) return ;
96+
97+ const handleTouchMove = ( e : TouchEvent ) => {
98+ if (
99+ bottomSheetRef . current &&
100+ ! bottomSheetRef . current . contains ( e . target as Node )
101+ ) {
102+ e . preventDefault ( ) ;
103+ }
104+ } ;
105+
106+ const handleTouchStart = ( e : TouchEvent ) => {
107+ if ( / i P h o n e | i P a d | i P o d / i. test ( navigator . userAgent ) ) {
108+ e . stopPropagation ( ) ;
109+ }
110+ } ;
111+
112+ document . addEventListener ( 'touchmove' , handleTouchMove , { passive : false } ) ;
113+ document . addEventListener ( 'touchstart' , handleTouchStart , {
114+ passive : false
115+ } ) ;
116+
117+ return ( ) => {
118+ document . removeEventListener ( 'touchmove' , handleTouchMove ) ;
119+ document . removeEventListener ( 'touchstart' , handleTouchStart ) ;
120+ } ;
121+ } , [ isVisible ] ) ;
122+
70123 return (
71124 < >
72125 { isVisible && (
@@ -86,8 +139,9 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
86139 animate = { {
87140 y : initialHeightValue ,
88141 transition : {
89- type : 'tween' ,
90- ease : 'easeOut' ,
142+ type : 'spring' ,
143+ damping : 30 ,
144+ stiffness : 300 ,
91145 duration : 0.3
92146 }
93147 } }
@@ -99,24 +153,48 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
99153 duration : 0.3
100154 }
101155 } }
102- { ...( ! disableDrag && {
103- drag : 'y' ,
104- dragConstraints : { top : 0 } ,
105- onDragEnd : handleDragEnd
106- } ) }
107- style = { { y, height : bottomSheetHeight } }
108- className = { `fixed inset-x-0 bottom-0 z-50 mx-auto max-w-[640px] rounded-tl-3xl rounded-tr-3xl bg-white shadow-lg ${
156+ style = { {
157+ y,
158+ height : bottomSheetHeight
159+ } }
160+ className = { `fixed inset-x-0 bottom-0 z-50 mx-auto flex max-w-[640px] flex-col rounded-tl-3xl rounded-tr-3xl bg-white shadow-lg ${
109161 overflow ? overflow : 'overflow-hidden'
110162 } `}
111163 { ...props }
112164 >
113- { topBar && (
114- < div className = "topBar relative text-center" >
115- < div className = "drag-bar mx-auto my-2 h-1 w-10 rounded-full bg-gray-300" />
116- { topBar }
117- </ div >
118- ) }
119- { children }
165+ < motion . div
166+ ref = { dragHandleRef }
167+ { ...( ! disableDrag && {
168+ drag : 'y' ,
169+ dragConstraints : {
170+ top : - windowHeight * 0.5 ,
171+ bottom : windowHeight * 0.7
172+ } ,
173+ dragElastic : 0.1 ,
174+ onDragEnd : handleDragEnd ,
175+ whileDrag : { cursor : 'grabbing' }
176+ } ) }
177+ className = "relative cursor-grab active:cursor-grabbing"
178+ style = { { touchAction : disableDrag ? 'auto' : 'none' } }
179+ >
180+ < div className = "drag-bar mx-auto my-2 h-1 w-10 rounded-full bg-gray-300" />
181+ { topBar && (
182+ < div className = "topBar relative pb-2 text-center" > { topBar } </ div >
183+ ) }
184+ </ motion . div >
185+
186+ { /* 컨텐츠 영역 - 스크롤 가능하지만 드래그 불가 */ }
187+ < div
188+ className = { `flex-1 ${ overflow ? overflow : 'overflow-y-auto' } ` }
189+ style = { {
190+ touchAction : 'pan-y' , // 세로 스크롤만 허용
191+ WebkitOverflowScrolling : 'touch' // iOS 스크롤 최적화
192+ } }
193+ onTouchMove = { e => e . stopPropagation ( ) }
194+ onTouchStart = { e => e . stopPropagation ( ) }
195+ >
196+ { children }
197+ </ div >
120198 </ motion . div >
121199 ) }
122200 </ AnimatePresence >
0 commit comments