@@ -190,7 +190,62 @@ const MiniPlayer = (() => {
190190 } ;
191191 const MIN_W = 240 , MAX_W = 560 ;
192192
193- function _attachDrag ( el ) {
193+ // Spring physics snap animation
194+ function _springSnap ( el , fromX , fromY , toX , toY ) {
195+ // Cancel any running spring
196+ if ( el . _springRaf ) cancelAnimationFrame ( el . _springRaf ) ;
197+
198+ const stiffness = 280 ; // spring stiffness
199+ const damping = 22 ; // damping coefficient
200+ const mass = 1 ;
201+
202+ let x = fromX , y = fromY ;
203+ let vx = 0 , vy = 0 ;
204+ let lastTime = null ;
205+
206+ function step ( ts ) {
207+ if ( ! lastTime ) { lastTime = ts ; el . _springRaf = requestAnimationFrame ( step ) ; return ; }
208+ const dt = Math . min ( ( ts - lastTime ) / 1000 , 0.032 ) ; // cap at 32ms
209+ lastTime = ts ;
210+
211+ // Spring force
212+ const ax = ( - stiffness * ( x - toX ) - damping * vx ) / mass ;
213+ const ay = ( - stiffness * ( y - toY ) - damping * vy ) / mass ;
214+
215+ vx += ax * dt ;
216+ vy += ay * dt ;
217+ x += vx * dt ;
218+ y += vy * dt ;
219+
220+ el . style . left = x + 'px' ;
221+ el . style . top = y + 'px' ;
222+
223+ // Check if settled (velocity and displacement both tiny)
224+ const settled = Math . abs ( x - toX ) < 0.15 && Math . abs ( y - toY ) < 0.15
225+ && Math . abs ( vx ) < 0.5 && Math . abs ( vy ) < 0.5 ;
226+
227+ if ( ! settled && document . body . contains ( el ) ) {
228+ el . _springRaf = requestAnimationFrame ( step ) ;
229+ } else {
230+ el . style . left = toX + 'px' ;
231+ el . style . top = toY + 'px' ;
232+ el . _springRaf = null ;
233+ // Subtle landing pulse
234+ el . style . transition = 'transform 0.18s cubic-bezier(0.34,1.56,0.64,1)' ;
235+ el . style . transform = 'scale(1.03)' ;
236+ setTimeout ( ( ) => {
237+ if ( el ) {
238+ el . style . transform = 'scale(1)' ;
239+ setTimeout ( ( ) => { if ( el ) el . style . transition = '' ; } , 200 ) ;
240+ }
241+ } , 80 ) ;
242+ }
243+ }
244+
245+ el . _springRaf = requestAnimationFrame ( step ) ;
246+ }
247+
248+ function _attachDrag ( el ) {
194249 // Drag is initiated from the pill (pointer-events: auto), not the full handle container
195250 const handle = el . querySelector ( '.mp-drag-pill' ) ;
196251 if ( ! handle ) return ;
@@ -238,7 +293,7 @@ const MiniPlayer = (() => {
238293 _drag . active = false ;
239294 el . classList . remove ( 'is-dragging' ) ;
240295
241- // Snap to nearest corner
296+ // Determine nearest corner
242297 const W = window . innerWidth , H = window . innerHeight ;
243298 const elW = el . offsetWidth , elH = el . offsetHeight ;
244299 const curLeft = parseFloat ( el . style . left ) || ( W - elW - 28 ) ;
@@ -247,15 +302,14 @@ const MiniPlayer = (() => {
247302 const cy = curTop + elH / 2 ;
248303 const MARGIN = 20 ;
249304
250- const snapLeft = cx < W / 2 ? MARGIN : W - elW - MARGIN ;
251- const snapTop = cy < H / 2 ? MARGIN : H - elH - MARGIN ;
305+ const targetLeft = cx < W / 2 ? MARGIN : W - elW - MARGIN ;
306+ const targetTop = cy < H / 2 ? MARGIN : H - elH - MARGIN ;
252307
253- el . classList . add ( 'snap-back' ) ;
254- el . style . left = snapLeft + 'px' ;
255- el . style . top = snapTop + 'px' ;
256308 el . style . right = 'auto' ;
257309 el . style . bottom = 'auto' ;
258- setTimeout ( ( ) => el && el . classList . remove ( 'snap-back' ) , 450 ) ;
310+
311+ // JS spring animation — much better than CSS transition
312+ _springSnap ( el , curLeft , curTop , targetLeft , targetTop ) ;
259313 }
260314
261315 handle . addEventListener ( 'mousedown' , onPointerDown ) ;
0 commit comments