} from '~/components/primitives/hooks';
import { Portal as RNPPortal } from '~/components/primitives/portal';
import * as Slot from '~/components/primitives/slot';
import * as React from 'react';
type AccessibilityActionEvent,
type GestureResponderEvent,
interface IRootContext extends RootProps {
onOpenChange: (open: boolean) => void;
pressPosition: LayoutPosition | null;
setPressPosition: (pressPosition: LayoutPosition | null) => void;
contentLayout: LayoutRectangle | null;
setContentLayout: (contentLayout: LayoutRectangle | null) => void;
const RootContext = React.createContext<IRootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>(
({ asChild, relativeTo = 'longPress', onOpenChange: onOpenChangeProp, ...viewProps }, ref) => {
const nativeID = React.useId();
const [pressPosition, setPressPosition] = React.useState<LayoutPosition | null>(null);
const [contentLayout, setContentLayout] = React.useState<LayoutRectangle | null>(null);
const [open, setOpen] = React.useState(false);
function onOpenChange(value: boolean) {
onOpenChangeProp?.(value);
const Component = asChild ? Slot.View : View;
<Component ref={ref} {...viewProps} />
Root.displayName = 'RootNativeContextMenu';
function useRootContext() {
const context = React.useContext(RootContext);
'ContextMenu compound components cannot be rendered outside the ContextMenu component'
const accessibilityActions = [{ name: 'longpress' }];
const Trigger = React.forwardRef<TriggerRef, TriggerProps>(
onLongPress: onLongPressProp,
onAccessibilityAction: onAccessibilityActionProp,
const { open, onOpenChange, relativeTo, setPressPosition } = useRootContext();
const augmentedRef = useAugmentedRef({
augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
setPressPosition({ width, pageX, pageY: pageY, height });
function onLongPress(ev: GestureResponderEvent) {
if (relativeTo === 'longPress') {
pageX: ev.nativeEvent.pageX,
pageY: ev.nativeEvent.pageY,
if (relativeTo === 'trigger') {
augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
setPressPosition({ width, pageX, pageY: pageY, height });
function onAccessibilityAction(event: AccessibilityActionEvent) {
if (event.nativeEvent.actionName === 'longpress') {
onAccessibilityActionProp?.(event);
const Component = asChild ? Slot.Pressable : Pressable;
aria-disabled={disabled ?? undefined}
onLongPress={onLongPress}
disabled={disabled ?? undefined}
accessibilityActions={accessibilityActions}
onAccessibilityAction={onAccessibilityAction}
Trigger.displayName = 'TriggerNativeContextMenu';
* @warning when using a custom `<PortalHost />`, you will have to adjust the Content's sideOffset to account for nav elements like headers.
function Portal({ forceMount, hostName, children }: PortalProps) {
const value = useRootContext();
if (!value.pressPosition) {
<RNPPortal hostName={hostName} name={`${value.nativeID}_portal`}>
<RootContext.Provider value={value}>{children}</RootContext.Provider>
const Overlay = React.forwardRef<OverlayRef, OverlayProps>(
({ asChild, forceMount, onPress: OnPressProp, closeOnPress = true, ...props }, ref) => {
const { open, onOpenChange, setContentLayout, setPressPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) {
const Component = asChild ? Slot.Pressable : Pressable;
return <Component ref={ref} onPress={onPress} {...props} />;
Overlay.displayName = 'OverlayNativeContextMenu';
const Content = React.forwardRef<ContentRef, ContentProps>(
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
const positionStyle = useRelativePosition({
triggerPosition: pressPosition,
function onLayout(event: LayoutChangeEvent) {
setContentLayout(event.nativeEvent.layout);
const Component = asChild ? Slot.View : View;
style={[positionStyle, style]}
onStartShouldSetResponder={onStartShouldSetResponder}
Content.displayName = 'ContentNativeContextMenu';
const Item = React.forwardRef<ItemRef, ItemProps>(
{ asChild, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props },
const { onOpenChange, setContentLayout, setPressPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) {
const Component = asChild ? Slot.Pressable : Pressable;
aria-valuetext={textValue}
aria-disabled={!!disabled}
accessibilityState={{ disabled: !!disabled }}
Item.displayName = 'ItemNativeContextMenu';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => {
const Component = asChild ? Slot.View : View;
return <Component ref={ref} role='group' {...props} />;
Group.displayName = 'GroupNativeContextMenu';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => {
const Component = asChild ? Slot.Text : Text;
return <Component ref={ref} {...props} />;
Label.displayName = 'LabelNativeContextMenu';
value: string | undefined;
onValueChange: (value: string) => void;
const FormItemContext = React.createContext<FormItemContext | null>(null);
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>(
const { onOpenChange, setContentLayout, setPressPosition, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) {
onCheckedChange(!checked);
const Component = asChild ? Slot.Pressable : Pressable;
<FormItemContext.Provider value={{ checked }}>
aria-disabled={!!disabled}
aria-valuetext={textValue}
accessibilityState={{ disabled: !!disabled }}
</FormItemContext.Provider>
CheckboxItem.displayName = 'CheckboxItemNativeContextMenu';
function useFormItemContext() {
const context = React.useContext(FormItemContext);
'CheckboxItem or RadioItem compound components cannot be rendered outside of a CheckboxItem or RadioItem component'
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>(
({ asChild, value, onValueChange, ...props }, ref) => {
const Component = asChild ? Slot.View : View;
<FormItemContext.Provider value={{ value, onValueChange }}>
<Component ref={ref} role='radiogroup' {...props} />
</FormItemContext.Provider>
RadioGroup.displayName = 'RadioGroupNativeContextMenu';
type BothFormItemContext = Exclude<FormItemContext, { checked: boolean }> & {
const RadioItemContext = React.createContext({} as { itemValue: string });
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>(
const { onOpenChange, setContentLayout, setPressPosition } = useRootContext();
const { value, onValueChange } = useFormItemContext() as BothFormItemContext;
function onPress(ev: GestureResponderEvent) {
onValueChange(itemValue);
const Component = asChild ? Slot.Pressable : Pressable;
<RadioItemContext.Provider value={{ itemValue }}>
aria-checked={value === itemValue}
disabled={disabled ?? false}
disabled: disabled ?? false,
checked: value === itemValue,
aria-valuetext={textValue}
</RadioItemContext.Provider>
RadioItem.displayName = 'RadioItemNativeContextMenu';
function useItemIndicatorContext() {
return React.useContext(RadioItemContext);
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>(
({ asChild, forceMount, ...props }, ref) => {
const { itemValue } = useItemIndicatorContext();
const { checked, value } = useFormItemContext() as BothFormItemContext;
if (itemValue == null && !checked) {
if (value !== itemValue) {
const Component = asChild ? Slot.View : View;
return <Component ref={ref} role='presentation' {...props} />;
ItemIndicator.displayName = 'ItemIndicatorNativeContextMenu';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>(
({ asChild, decorative, ...props }, ref) => {
const Component = asChild ? Slot.View : View;
return <Component role={decorative ? 'presentation' : 'separator'} ref={ref} {...props} />;
Separator.displayName = 'SeparatorNativeContextMenu';
const SubContext = React.createContext<{
onOpenChange: (value: boolean) => void;
const Sub = React.forwardRef<SubRef, SubProps>(
({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => {
const nativeID = React.useId();
const [open = false, onOpenChange] = useControllableState({
defaultProp: defaultOpen,
onChange: onOpenChangeProp,
const Component = asChild ? Slot.View : View;
<Component ref={ref} {...props} />
Sub.displayName = 'SubNativeContextMenu';
function useSubContext() {
const context = React.useContext(SubContext);
throw new Error('Sub compound components cannot be rendered outside of a Sub component');
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>(
({ asChild, textValue, onPress: onPressProp, disabled = false, ...props }, ref) => {
const { nativeID, open, onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) {
const Component = asChild ? Slot.Pressable : Pressable;
aria-valuetext={textValue}
accessibilityState={{ expanded: open, disabled: !!disabled }}
aria-disabled={!!disabled}
SubTrigger.displayName = 'SubTriggerNativeContextMenu';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>(
({ asChild = false, forceMount, ...props }, ref) => {
const { open, nativeID } = useSubContext();
const Component = asChild ? Slot.Pressable : Pressable;
return <Component ref={ref} role='group' aria-labelledby={nativeID} {...props} />;
Content.displayName = 'ContentNativeContextMenu';
function onStartShouldSetResponder() {