Skip to content

Commit 3add3d6

Browse files
committed
Update use-drag-to-corner.ts
1 parent 0d98d40 commit 3add3d6

File tree

1 file changed

+60
-57
lines changed

1 file changed

+60
-57
lines changed

packages/ui/src/components/devPrompts/KeylessPrompt/use-drag-to-corner.ts

Lines changed: 60 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { PointerEventHandler } from 'react';
2-
import { useCallback, useEffect, useRef, useState } from 'react';
2+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
33

44
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
55

66
const 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;
99
const CORNER_OFFSET = '1.25rem';
10-
const DRAG_THRESHOLD = 5; // Minimum pixels to move before starting drag
10+
const DRAG_THRESHOLD = 5;
1111

1212
interface 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
2726
const 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
3230
const 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
4649
const 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)
6062
const 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
7576
const 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
8991
const 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

Comments
 (0)