POWR/components/ui/navigation-menu.tsx

182 lines
6.3 KiB
TypeScript
Raw Permalink Normal View History

2025-02-09 20:38:38 -05:00
import * as NavigationMenuPrimitive from '@rn-primitives/navigation-menu';
import { cva } from 'class-variance-authority';
import * as React from 'react';
import { Platform, View } from 'react-native';
import Animated, {
Extrapolation,
FadeInLeft,
FadeOutLeft,
interpolate,
useAnimatedStyle,
useDerivedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown } from '@/lib/icons/ChevronDown';
import { cn } from '@/lib/utils';
const NavigationMenu = React.forwardRef<
NavigationMenuPrimitive.RootRef,
NavigationMenuPrimitive.RootProps
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn('relative z-10 flex flex-row max-w-max items-center justify-center', className)}
{...props}
>
{children}
{Platform.OS === 'web' && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
NavigationMenuPrimitive.ListRef,
NavigationMenuPrimitive.ListProps
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
'web:group flex flex-1 flex-row web:list-none items-center justify-center gap-1',
className
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
'web:group web:inline-flex flex-row h-10 native:h-12 native:px-3 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium web:transition-colors web:hover:bg-accent active:bg-accent web:hover:text-accent-foreground web:focus:bg-accent web:focus:text-accent-foreground web:focus:outline-none web:disabled:pointer-events-none disabled:opacity-50 web:data-[active]:bg-accent/50 web:data-[state=open]:bg-accent/50'
);
const NavigationMenuTrigger = React.forwardRef<
NavigationMenuPrimitive.TriggerRef,
NavigationMenuPrimitive.TriggerProps
>(({ className, children, ...props }, ref) => {
const { value } = NavigationMenuPrimitive.useRootContext();
const { value: itemValue } = NavigationMenuPrimitive.useItemContext();
const progress = useDerivedValue(() =>
value === itemValue ? withTiming(1, { duration: 250 }) : withTiming(0, { duration: 200 })
);
const chevronStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${progress.value * 180}deg` }],
opacity: interpolate(progress.value, [0, 1], [1, 0.8], Extrapolation.CLAMP),
}));
return (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(
navigationMenuTriggerStyle(),
'web:group gap-1.5',
value === itemValue && 'bg-accent',
className
)}
{...props}
>
<>{children}</>
<Animated.View style={chevronStyle}>
<ChevronDown
size={12}
className={cn('relative text-foreground h-3 w-3 web:transition web:duration-200')}
aria-hidden={true}
/>
</Animated.View>
</NavigationMenuPrimitive.Trigger>
);
});
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
NavigationMenuPrimitive.ContentRef,
NavigationMenuPrimitive.ContentProps & {
portalHost?: string;
}
>(({ className, children, portalHost, ...props }, ref) => {
const { value } = NavigationMenuPrimitive.useRootContext();
const { value: itemValue } = NavigationMenuPrimitive.useItemContext();
return (
<NavigationMenuPrimitive.Portal hostName={portalHost}>
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
'w-full native:border native:border-border native:rounded-lg native:shadow-lg native:bg-popover native:text-popover-foreground native:overflow-hidden',
value === itemValue
? 'web:animate-in web:fade-in web:slide-in-from-right-20'
: 'web:animate-out web:fade-out web:slide-out-to-left-20',
className
)}
{...props}
>
<Animated.View
entering={Platform.OS !== 'web' ? FadeInLeft : undefined}
exiting={Platform.OS !== 'web' ? FadeOutLeft : undefined}
>
{children}
</Animated.View>
</NavigationMenuPrimitive.Content>
</NavigationMenuPrimitive.Portal>
);
});
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
NavigationMenuPrimitive.ViewportRef,
NavigationMenuPrimitive.ViewportProps
>(({ className, ...props }, ref) => {
return (
<View className={cn('absolute left-0 top-full flex justify-center')}>
<View
className={cn(
'web:origin-top-center relative mt-1.5 web:h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg web:animate-in web:zoom-in-90',
className
)}
ref={ref}
{...props}
>
<NavigationMenuPrimitive.Viewport />
</View>
</View>
);
});
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
NavigationMenuPrimitive.IndicatorRef,
NavigationMenuPrimitive.IndicatorProps
>(({ className, ...props }, ref) => {
const { value } = NavigationMenuPrimitive.useRootContext();
const { value: itemValue } = NavigationMenuPrimitive.useItemContext();
return (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
value === itemValue ? 'web:animate-in web:fade-in' : 'web:animate-out web:fade-out',
className
)}
{...props}
>
<View className='relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md shadow-foreground/5' />
</NavigationMenuPrimitive.Indicator>
);
});
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
NavigationMenu,
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
NavigationMenuViewport,
};