(null);
const shouldCancelInteractionRef = React.useRef(false);
function handleStartCycle() {
// Stop if this is the second click of a double click
if (shouldCancelInteractionRef.current) {
handleCancelInteraction();
return;
}
window.setTimeout(() => {
handleCycleSnapPoints();
}, DOUBLE_TAP_TIMEOUT);
}
function handleCycleSnapPoints() {
// Prevent accidental taps while resizing drawer
if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
handleCancelInteraction();
return;
}
// Make sure to clear the timeout id if the user releases the handle before the cancel timeout
handleCancelInteraction();
if (!snapPoints || snapPoints.length === 0) {
if (!dismissible) {
closeDrawer();
}
return;
}
const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
if (isLastSnapPoint && dismissible) {
closeDrawer();
return;
}
const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
const nextSnapPoint = snapPoints[currentSnapIndex + 1];
if (nextSnapPoint === undefined) return;
setActiveSnapPoint(nextSnapPoint);
}
function handleStartInteraction() {
closeTimeoutIdRef.current = window.setTimeout(() => {
// Cancel click interaction on a long press
shouldCancelInteractionRef.current = true;
}, LONG_HANDLE_PRESS_TIMEOUT);
}
function handleCancelInteraction() {
if (closeTimeoutIdRef.current) {
window.clearTimeout(closeTimeoutIdRef.current);
}
shouldCancelInteractionRef.current = false;
}
return (
{
if (handleOnly) onPress(e);
handleStartInteraction();
}}
onPointerMove={(e) => {
if (handleOnly) onDrag(e);
}}
// onPointerUp is already handled by the content component
ref={ref}
data-vaul-drawer-visible={isOpen ? 'true' : 'false'}
data-vaul-handle=""
aria-hidden="true"
{...rest}
>
{/* Expand handle's hit area beyond what's visible to ensure a 44x44 tap target for touch devices */}
{children}
);
}
);
Handle.displayName = 'Drawer.Handle';
export function NestedRoot({ onDrag, onOpenChange, open: nestedIsOpen, ...rest }: DialogProps) {
const { onNestedDrag, onNestedOpenChange, onNestedRelease } = useDrawerContext();
if (!onNestedDrag) {
throw new Error('Drawer.NestedRoot must be placed in another drawer');
}
return (
{
onNestedOpenChange(false);
}}
onDrag={(e, p) => {
onNestedDrag(e, p);
onDrag?.(e, p);
}}
onOpenChange={(o) => {
if (o) {
onNestedOpenChange(o);
}
onOpenChange?.(o);
}}
onRelease={onNestedRelease}
{...rest}
/>
);
}
type PortalProps = React.ComponentPropsWithoutRef;
export function Portal(props: PortalProps) {
const context = useDrawerContext();
const { container = context.container, ...portalProps } = props;
return ;
}
export const Drawer = {
Root,
NestedRoot,
Content,
Overlay,
Trigger: DialogPrimitive.Trigger,
Portal,
Handle,
Close: DialogPrimitive.Close,
Title: DialogPrimitive.Title,
Description: DialogPrimitive.Description,
};