@ -243,6 +243,7 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
const headerRef = useRef < HTMLDivElement > ( null ) ;
const contentRef = useRef < HTMLDivElement > ( null ) ;
const sheetSizeRef = useRef ( SHEET_INITIAL_SIZE ) ;
const restingStateRef = useRef < number > ( SHEET_INITIAL_SIZE ) ;
const termsTitle = t [ "terms & conditions" ] || "Terms & Conditions" ;
const gotItLabel = t [ "Got it" ] || "Got it" ;
@ -269,7 +270,9 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
isClosingRef . current = false ;
setIsClosing ( false ) ;
sheetSizeRef . current = SHEET_INITIAL_SIZE ;
restingStateRef . current = SHEET_INITIAL_SIZE ;
if ( sheetRef . current ) {
sheetRef . current . style . transition = "" ;
sheetRef . current . style . setProperty (
"--sheet-size" ,
SHEET_INITIAL_SIZE . toFixed ( 4 ) ,
@ -295,6 +298,7 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
// Drag-to-resize parity with Flutter DraggableScrollableSheet:
// initialChildSize: 0.75, minChildSize: 0.6, maxChildSize: 1.0, shouldCloseOnMinExtent: true
// Snapping: Exactly two resting states (0.75 default and 1.0 expanded below status bar).
useEffect ( ( ) = > {
if ( ! isOpen || isClosing ) return ;
@ -310,10 +314,25 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
let startX = 0 ;
let startY = 0 ;
let lastY = 0 ;
let snapTimeout : ReturnType < typeof setTimeout > | null = null ;
let wheelSnapTimeout : ReturnType < typeof setTimeout > | null = null ;
const contentOverflows = ( ) = >
content ? content . scrollHeight > content . clientHeight + 1 : false ;
const snapTo = ( targetSize : number ) = > {
sheetSizeRef . current = targetSize ;
restingStateRef . current = targetSize ;
sheet . style . transition = "height 260ms cubic-bezier(0.16, 1, 0.3, 1)" ;
sheet . style . setProperty ( "--sheet-size" , targetSize . toFixed ( 4 ) ) ;
if ( snapTimeout ) clearTimeout ( snapTimeout ) ;
snapTimeout = setTimeout ( ( ) = > {
if ( sheet ) {
sheet . style . transition = "" ;
}
} , 270 ) ;
} ;
const resize = ( deltaPx : number ) = > {
const viewportHeight = window . innerHeight || 1 ;
const nextSize = Math . min (
@ -332,6 +351,11 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
active = false ;
return ;
}
if ( snapTimeout ) {
clearTimeout ( snapTimeout ) ;
snapTimeout = null ;
}
sheet . style . transition = "none" ;
active = true ;
engaged = fromHeader ;
isHeaderDrag = fromHeader ;
@ -385,9 +409,49 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
} ;
const handleTouchEnd = ( ) = > {
if ( ! active || closing ) {
active = false ;
engaged = false ;
isHeaderDrag = false ;
return ;
}
active = false ;
engaged = false ;
isHeaderDrag = false ;
const currentSize = sheetSizeRef . current ;
const baseline = restingStateRef . current ;
const totalPulled = startY - lastY ; // > 0 finger pulled up, < 0 finger pulled down
// Flutter shouldCloseOnMinExtent: true when dragged near or below 60% floor
if ( currentSize <= SHEET_MIN_SIZE + 0.01 ) {
closing = true ;
closeSheet ( ) ;
return ;
}
// Snapping between only two states: 0.75 (default) and 1.0 (expanded)
if ( baseline === SHEET_MAX_SIZE ) {
// Dragging down from 1.0: if pulled down >= 30px or size <= 0.94, snap to 0.75
if ( totalPulled <= - 30 || currentSize <= 0.94 ) {
snapTo ( SHEET_INITIAL_SIZE ) ;
} else {
snapTo ( SHEET_MAX_SIZE ) ;
}
} else {
// Dragging from 0.75:
// If pulled down strongly, close
if ( totalPulled <= - 60 ) {
closing = true ;
closeSheet ( ) ;
} else if ( totalPulled >= 30 || currentSize >= 0.78 ) {
// Dragged up enough to expand to 1.0
snapTo ( SHEET_MAX_SIZE ) ;
} else {
// Snap back to initial 0.75
snapTo ( SHEET_INITIAL_SIZE ) ;
}
}
} ;
const handleWheel = ( event : WheelEvent ) = > {
@ -412,6 +476,20 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
} else {
resize ( delta ) ;
}
if ( wheelSnapTimeout ) clearTimeout ( wheelSnapTimeout ) ;
wheelSnapTimeout = setTimeout ( ( ) = > {
if ( closing ) return ;
const current = sheetSizeRef . current ;
if ( current <= SHEET_MIN_SIZE + 0.01 ) {
closing = true ;
closeSheet ( ) ;
} else if ( current >= 0.82 ) {
snapTo ( SHEET_MAX_SIZE ) ;
} else {
snapTo ( SHEET_INITIAL_SIZE ) ;
}
} , 160 ) ;
} ;
const onHeaderTouchStart = ( e : TouchEvent ) = > handleTouchStart ( e , true ) ;
@ -436,6 +514,8 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
sheet . addEventListener ( "touchcancel" , handleTouchEnd , { passive : true } ) ;
return ( ) = > {
if ( snapTimeout ) clearTimeout ( snapTimeout ) ;
if ( wheelSnapTimeout ) clearTimeout ( wheelSnapTimeout ) ;
if ( header ) {
header . removeEventListener ( "touchstart" , onHeaderTouchStart ) ;
}
@ -477,9 +557,15 @@ export function TermsSheet({ isOpen = false, onClose }: TermsSheetProps) {
>
< section
ref = { sheetRef }
style = { {
height :
"calc(var(--sheet-size, 0.75) * (100svh - max(var(--safe-top, 0px), env(safe-area-inset-top, 0px))))" ,
maxHeight :
"calc(100svh - max(var(--safe-top, 0px), env(safe-area-inset-top, 0px)))" ,
} }
className = { [
"flex w-full max-w-[834px] sm:max-w-[540px] flex-col overflow-hidden rounded-t-[22px] bg-white shadow-[0_-10px_32px_rgba(0,0,0,0.12)]" ,
"h-[calc(var(--sheet-size,0.75)*100svh)]" ,
"flutter-draggable-sheet " ,
isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface" ,
] . join ( " " ) }
>