11import type { PointerEventHandler } from 'react' ;
2- import { useCallback , useEffect , useRef , useState } from 'react' ;
2+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
33
44type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' ;
55
66const STORAGE_KEY = 'clerk-keyless-prompt-corner' ;
7- const LERP_FACTOR = 0.15 ; // Smooth trailing effect
8- const INERTIA_MULTIPLIER = 8 ; // Velocity projection multiplier
7+ const LERP_FACTOR = 0.15 ;
8+ const INERTIA_MULTIPLIER = 8 ;
99const CORNER_OFFSET = '1.25rem' ;
10- const DRAG_THRESHOLD = 5 ; // Minimum pixels to move before starting drag
10+ const DRAG_THRESHOLD = 5 ;
1111
1212interface Position {
1313 x : number ;
@@ -20,29 +20,32 @@ interface UseDragToCornerResult {
2020 style : React . CSSProperties ;
2121 containerRef : React . RefObject < HTMLDivElement > ;
2222 onPointerDown : PointerEventHandler ;
23- preventClick : boolean ; // Flag to prevent click events after drag
23+ preventClick : boolean ;
2424}
2525
26- // Lerp utility for smooth interpolation
2726const lerp = ( start : number , end : number , factor : number ) : number => {
2827 return start + ( end - start ) * factor ;
2928} ;
3029
31- // Determine corner based on position relative to viewport center
3230const getCornerFromPosition = ( x : number , y : number ) : Corner => {
3331 const centerX = window . innerWidth / 2 ;
3432 const centerY = window . innerHeight / 2 ;
3533
3634 const isLeft = x < centerX ;
3735 const isTop = y < centerY ;
3836
39- if ( isTop && isLeft ) return 'top-left' ;
40- if ( isTop && ! isLeft ) return 'top-right' ;
41- if ( ! isTop && isLeft ) return 'bottom-left' ;
37+ if ( isTop && isLeft ) {
38+ return 'top-left' ;
39+ }
40+ if ( isTop && ! isLeft ) {
41+ return 'top-right' ;
42+ }
43+ if ( ! isTop && isLeft ) {
44+ return 'bottom-left' ;
45+ }
4246 return 'bottom-right' ;
4347} ;
4448
45- // Get CSS styles for a corner position
4649const getCornerStyles = ( corner : Corner ) : React . CSSProperties => {
4750 switch ( corner ) {
4851 case 'top-left' :
@@ -56,9 +59,8 @@ const getCornerStyles = (corner: Corner): React.CSSProperties => {
5659 }
5760} ;
5861
59- // Get corner position in pixels (for smooth transition)
6062const getCornerPositionInPixels = ( corner : Corner , elementWidth : number , elementHeight : number ) : Position => {
61- const offset = 20 ; // 1.25rem ≈ 20px
63+ const offset = 20 ;
6264 switch ( corner ) {
6365 case 'top-left' :
6466 return { x : offset , y : offset } ;
@@ -71,9 +73,10 @@ const getCornerPositionInPixels = (corner: Corner, elementWidth: number, element
7173 }
7274} ;
7375
74- // Load corner preference from localStorage
7576const loadCornerPreference = ( ) : Corner => {
76- if ( typeof window === 'undefined' ) return 'bottom-right' ;
77+ if ( typeof window === 'undefined' ) {
78+ return 'bottom-right' ;
79+ }
7780 try {
7881 const stored = localStorage . getItem ( STORAGE_KEY ) ;
7982 if ( stored && [ 'top-left' , 'top-right' , 'bottom-left' , 'bottom-right' ] . includes ( stored ) ) {
@@ -85,9 +88,10 @@ const loadCornerPreference = (): Corner => {
8588 return 'bottom-right' ;
8689} ;
8790
88- // Save corner preference to localStorage
8991const saveCornerPreference = ( corner : Corner ) : void => {
90- if ( typeof window === 'undefined' ) return ;
92+ if ( typeof window === 'undefined' ) {
93+ return ;
94+ }
9195 try {
9296 localStorage . setItem ( STORAGE_KEY , corner ) ;
9397 } catch {
@@ -103,6 +107,7 @@ export const useDragToCorner = (): UseDragToCornerResult => {
103107
104108 const containerRef = useRef < HTMLDivElement | null > ( null ) ;
105109 const animationFrameRef = useRef < number | null > ( null ) ;
110+ const transitionTimeoutRef = useRef < number | null > ( null ) ;
106111 const targetPosRef = useRef < Position > ( { x : 0 , y : 0 } ) ;
107112 const currentPosRef = useRef < Position > ( { x : 0 , y : 0 } ) ;
108113 const lastPosRef = useRef < Position > ( { x : 0 , y : 0 } ) ;
@@ -112,55 +117,55 @@ export const useDragToCorner = (): UseDragToCornerResult => {
112117 const lastTimeRef = useRef < number > ( 0 ) ;
113118 const hasStartedDraggingRef = useRef < boolean > ( false ) ;
114119
115- // Animation loop for lerp-based dragging
116120 const animate = useCallback ( ( ) => {
121+ const container = containerRef . current ;
122+ if ( ! container ) {
123+ return ;
124+ }
125+
117126 const current = currentPosRef . current ;
118127 const target = targetPosRef . current ;
119128
120- // Lerp current position towards target
121129 current . x = lerp ( current . x , target . x , LERP_FACTOR ) ;
122130 current . y = lerp ( current . y , target . y , LERP_FACTOR ) ;
123131
124- // Calculate velocity from position delta
125132 const now = performance . now ( ) ;
126- const deltaTime = Math . max ( now - lastTimeRef . current , 1 ) ; // Prevent division by zero
133+ const deltaTime = Math . max ( now - lastTimeRef . current , 1 ) ;
127134 const deltaX = current . x - lastPosRef . current . x ;
128135 const deltaY = current . y - lastPosRef . current . y ;
129136
130- velocityRef . current . x = deltaX / ( deltaTime / 16.67 ) ; // Normalize to 60fps
137+ velocityRef . current . x = deltaX / ( deltaTime / 16.67 ) ;
131138 velocityRef . current . y = deltaY / ( deltaTime / 16.67 ) ;
132139
133- lastPosRef . current = { ...current } ;
140+ lastPosRef . current . x = current . x ;
141+ lastPosRef . current . y = current . y ;
134142 lastTimeRef . current = now ;
135143
136- // Update position style
137- setDragStyle ( {
138- position : 'fixed' ,
139- left : `${ current . x } px` ,
140- top : `${ current . y } px` ,
141- transition : 'none' , // No transition during drag
142- } ) ;
144+ // Direct DOM manipulation instead of setState
145+ container . style . position = 'fixed' ;
146+ container . style . left = `${ current . x } px` ;
147+ container . style . top = `${ current . y } px` ;
148+ container . style . transition = 'none' ;
143149
144150 animationFrameRef . current = requestAnimationFrame ( animate ) ;
145151 } , [ ] ) ;
146152
147- // Start drag
148153 const handlePointerDown : PointerEventHandler = useCallback (
149154 e => {
150- // Only allow dragging on the button/header area, not on links
151155 const target = e . target as HTMLElement ;
152156 if ( target . tagName === 'A' || target . closest ( 'a' ) ) {
153157 return ;
154158 }
155159
156160 const container = containerRef . current ;
157- if ( ! container ) return ;
161+ if ( ! container ) {
162+ return ;
163+ }
158164
159165 const rect = container . getBoundingClientRect ( ) ;
160166 const startX = e . clientX ;
161167 const startY = e . clientY ;
162168
163- // Initialize positions
164169 startPosRef . current = { x : startX , y : startY } ;
165170 startOffsetRef . current = { x : rect . left , y : rect . top } ;
166171 currentPosRef . current = { x : rect . left , y : rect . top } ;
@@ -170,22 +175,18 @@ export const useDragToCorner = (): UseDragToCornerResult => {
170175 lastTimeRef . current = performance . now ( ) ;
171176 hasStartedDraggingRef . current = false ;
172177
173- // Handle pointer move
174178 const handlePointerMove = ( moveEvent : PointerEvent ) => {
175179 const deltaX = moveEvent . clientX - startPosRef . current . x ;
176180 const deltaY = moveEvent . clientY - startPosRef . current . y ;
177181 const distance = Math . sqrt ( deltaX * deltaX + deltaY * deltaY ) ;
178182
179- // Only start dragging if moved beyond threshold
180183 if ( ! hasStartedDraggingRef . current && distance < DRAG_THRESHOLD ) {
181184 return ;
182185 }
183186
184187 if ( ! hasStartedDraggingRef . current ) {
185- // Start dragging now
186188 hasStartedDraggingRef . current = true ;
187189 setIsDragging ( true ) ;
188- // Start animation loop
189190 animationFrameRef . current = requestAnimationFrame ( animate ) ;
190191 }
191192
@@ -196,52 +197,50 @@ export const useDragToCorner = (): UseDragToCornerResult => {
196197 } ;
197198 } ;
198199
199- // Handle pointer up
200200 const handlePointerUp = ( ) => {
201201 window . removeEventListener ( 'pointermove' , handlePointerMove ) ;
202202 window . removeEventListener ( 'pointerup' , handlePointerUp ) ;
203203
204- // Stop animation loop
205204 if ( animationFrameRef . current !== null ) {
206205 cancelAnimationFrame ( animationFrameRef . current ) ;
207206 animationFrameRef . current = null ;
208207 }
209208
210- // Only process drag end if we actually started dragging
211209 if ( hasStartedDraggingRef . current ) {
212210 setIsDragging ( false ) ;
213211 setPreventClick ( true ) ;
214212
215- // Project final position with inertia
216213 const current = currentPosRef . current ;
217214 const velocity = velocityRef . current ;
218215 const projectedX = current . x + velocity . x * INERTIA_MULTIPLIER ;
219216 const projectedY = current . y + velocity . y * INERTIA_MULTIPLIER ;
220217
221- // Determine target corner
222218 const newCorner = getCornerFromPosition ( projectedX , projectedY ) ;
223219
224- // Get the target corner position in pixels for smooth transition
225220 const rect = container . getBoundingClientRect ( ) ;
226221 const targetPos = getCornerPositionInPixels ( newCorner , rect . width , rect . height ) ;
227222
228- // Animate to corner position smoothly
229223 setDragStyle ( {
230224 position : 'fixed' ,
231225 left : `${ targetPos . x } px` ,
232226 top : `${ targetPos . y } px` ,
233- transition : 'all 400ms cubic-bezier(0.2, 0, 0.2, 1)' , // Smooth ease-in-out
227+ transition : 'all 400ms cubic-bezier(0.2, 0, 0.2, 1)' ,
234228 } ) ;
235229
236- // Update corner and save preference
237230 setCorner ( newCorner ) ;
238231 saveCornerPreference ( newCorner ) ;
239232
240- // After transition completes, switch to corner-based positioning
241- setTimeout ( ( ) => {
233+ transitionTimeoutRef . current = window . setTimeout ( ( ) => {
242234 setDragStyle ( { } ) ;
243235 setPreventClick ( false ) ;
244- } , 400 ) ; // Match transition duration
236+ // Clear inline styles to return to React-controlled positioning
237+ if ( container ) {
238+ container . style . position = '' ;
239+ container . style . left = '' ;
240+ container . style . top = '' ;
241+ container . style . transition = '' ;
242+ }
243+ } , 400 ) ;
245244 }
246245
247246 hasStartedDraggingRef . current = false ;
@@ -253,21 +252,25 @@ export const useDragToCorner = (): UseDragToCornerResult => {
253252 [ animate ] ,
254253 ) ;
255254
256- // Cleanup animation frame on unmount
257255 useEffect ( ( ) => {
258256 return ( ) => {
259257 if ( animationFrameRef . current !== null ) {
260258 cancelAnimationFrame ( animationFrameRef . current ) ;
261259 }
260+ if ( transitionTimeoutRef . current !== null ) {
261+ clearTimeout ( transitionTimeoutRef . current ) ;
262+ }
262263 } ;
263264 } , [ ] ) ;
264265
265- // Combine corner styles with drag styles
266- const style : React . CSSProperties = {
267- ...getCornerStyles ( corner ) ,
268- ...dragStyle , // Always apply dragStyle (empty when not dragging/snapping)
269- transition : isDragging ? 'none' : dragStyle . transition || 'all 250ms cubic-bezier(0.2, 0, 0.2, 1)' , // Use dragStyle transition if present
270- } ;
266+ const style = useMemo < React . CSSProperties > (
267+ ( ) => ( {
268+ ...getCornerStyles ( corner ) ,
269+ ...dragStyle ,
270+ transition : isDragging ? 'none' : dragStyle . transition || 'all 250ms cubic-bezier(0.2, 0, 0.2, 1)' ,
271+ } ) ,
272+ [ corner , isDragging , dragStyle ] ,
273+ ) ;
271274
272275 return {
273276 corner,
0 commit comments