{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "native-internal-hook-use-relative-position", "type": "registry:hook", "title": "Use Relative Position", "description": "Use Relative Position from @pitsi-ui/native for native projects.", "dependencies": [ "@gorhom/bottom-sheet@^5.2.8", "react-native-reanimated@^4.1.1", "tailwind-variants@^3.2.2" ], "registryDependencies": [], "files": [ { "path": "registry/native-ui/src/helpers/internal/hooks/use-relative-position.ts", "content": "import * as React from \"react\";\nimport { Dimensions, type LayoutRectangle, type ScaledSize } from \"react-native\";\nimport type { Insets } from \"../types\";\n\ntype UseRelativePositionArgs = Omit<\n GetContentStyleArgs,\n \"triggerPosition\" | \"contentLayout\" | \"dimensions\"\n> & {\n triggerPosition: LayoutPosition | null;\n contentLayout: LayoutRectangle | null;\n disablePositioningStyle?: boolean;\n};\n\nexport function useRelativePosition({\n align,\n avoidCollisions,\n triggerPosition,\n contentLayout,\n alignOffset,\n insets,\n offset,\n placement,\n disablePositioningStyle,\n}: UseRelativePositionArgs) {\n const dimensions = Dimensions.get(\"screen\");\n\n return React.useMemo(() => {\n if (disablePositioningStyle) {\n return {};\n }\n if (!triggerPosition || !contentLayout) {\n return {\n position: \"absolute\",\n opacity: 0,\n top: dimensions.height,\n } as const;\n }\n return getContentStyle({\n align,\n avoidCollisions,\n contentLayout,\n placement,\n triggerPosition,\n alignOffset,\n insets,\n offset,\n dimensions,\n });\n }, [\n align,\n avoidCollisions,\n placement,\n alignOffset,\n insets,\n triggerPosition,\n contentLayout,\n dimensions,\n disablePositioningStyle,\n offset,\n ]);\n}\n\nexport interface LayoutPosition {\n pageY: number;\n pageX: number;\n width: number;\n height: number;\n}\n\ninterface GetPositionArgs {\n dimensions: ScaledSize;\n avoidCollisions: boolean;\n triggerPosition: LayoutPosition;\n contentLayout: LayoutRectangle;\n insets?: Insets;\n}\n\ninterface GetSidePositionArgs extends GetPositionArgs {\n placement: \"top\" | \"bottom\" | \"left\" | \"right\";\n offset: number;\n}\n\nfunction getSidePosition({\n placement,\n triggerPosition,\n contentLayout,\n offset,\n insets,\n avoidCollisions,\n dimensions,\n}: GetSidePositionArgs) {\n const insetTop = insets?.top ?? 0;\n const insetBottom = insets?.bottom ?? 0;\n const insetLeft = insets?.left ?? 0;\n const insetRight = insets?.right ?? 0;\n\n // Handle vertical sides (top/bottom)\n if (placement === \"top\" || placement === \"bottom\") {\n const positionTop = triggerPosition?.pageY - offset - contentLayout.height;\n const positionBottom = triggerPosition.pageY + triggerPosition.height + offset;\n\n if (!avoidCollisions) {\n return {\n top: placement === \"top\" ? positionTop : positionBottom,\n };\n }\n\n if (placement === \"top\") {\n return {\n top: Math.min(\n Math.max(insetTop, positionTop),\n dimensions.height - insetBottom - contentLayout.height,\n ),\n };\n }\n\n return {\n top: Math.min(dimensions.height - insetBottom - contentLayout.height, positionBottom),\n };\n }\n\n // Handle horizontal sides (left/right)\n const maxContentWidth = dimensions.width - insetLeft - insetRight;\n const contentWidth = Math.min(contentLayout.width, maxContentWidth);\n\n const positionLeft = triggerPosition.pageX - offset - contentWidth;\n const positionRight = triggerPosition.pageX + triggerPosition.width + offset;\n\n if (!avoidCollisions) {\n return {\n left: placement === \"left\" ? positionLeft : positionRight,\n };\n }\n\n if (placement === \"left\") {\n return {\n left: Math.min(\n Math.max(insetLeft, positionLeft),\n dimensions.width - insetRight - contentWidth,\n ),\n };\n }\n\n // For right placement, ensure content doesn't go beyond left inset\n return {\n left: Math.max(\n insetLeft,\n Math.min(dimensions.width - insetRight - contentWidth, positionRight),\n ),\n };\n}\n\ninterface GetAlignPositionArgs extends GetPositionArgs {\n align: \"start\" | \"center\" | \"end\";\n alignOffset: number;\n placement: \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\nfunction getAlignPosition({\n align,\n avoidCollisions,\n contentLayout,\n triggerPosition,\n alignOffset,\n insets,\n dimensions,\n placement,\n}: GetAlignPositionArgs) {\n const insetLeft = insets?.left ?? 0;\n const insetRight = insets?.right ?? 0;\n const insetTop = insets?.top ?? 0;\n const insetBottom = insets?.bottom ?? 0;\n\n // For top/bottom sides, align horizontally\n if (placement === \"top\" || placement === \"bottom\") {\n const maxContentWidth = dimensions.width - insetLeft - insetRight;\n const contentWidth = Math.min(contentLayout.width, maxContentWidth);\n\n let left = getHorizontalAlignPosition(\n align,\n triggerPosition.pageX,\n triggerPosition.width,\n contentWidth,\n alignOffset,\n insetLeft,\n insetRight,\n dimensions,\n );\n\n if (avoidCollisions) {\n const doesCollide = left < insetLeft || left + contentWidth > dimensions.width - insetRight;\n if (doesCollide) {\n const spaceLeft = left - insetLeft;\n const spaceRight = dimensions.width - insetRight - (left + contentWidth);\n\n if (spaceLeft > spaceRight && spaceLeft >= contentWidth) {\n left = insetLeft;\n } else if (spaceRight >= contentWidth) {\n left = dimensions.width - insetRight - contentWidth;\n } else {\n const centeredPosition = Math.max(\n insetLeft,\n (dimensions.width - contentWidth - insetRight) / 2,\n );\n left = centeredPosition;\n }\n }\n }\n\n return { left, maxWidth: maxContentWidth };\n }\n\n // For left/right sides, align vertically and constrain width\n const maxContentHeight = dimensions.height - insetTop - insetBottom;\n const maxContentWidth = dimensions.width - insetLeft - insetRight;\n const contentHeight = Math.min(contentLayout.height, maxContentHeight);\n\n let top = getVerticalAlignPosition(\n align,\n triggerPosition.pageY,\n triggerPosition.height,\n contentHeight,\n alignOffset,\n insetTop,\n insetBottom,\n dimensions,\n );\n\n if (avoidCollisions) {\n const doesCollide = top < insetTop || top + contentHeight > dimensions.height - insetBottom;\n if (doesCollide) {\n const spaceTop = top - insetTop;\n const spaceBottom = dimensions.height - insetBottom - (top + contentHeight);\n\n if (spaceTop > spaceBottom && spaceTop >= contentHeight) {\n top = insetTop;\n } else if (spaceBottom >= contentHeight) {\n top = dimensions.height - insetBottom - contentHeight;\n } else {\n const centeredPosition = Math.max(\n insetTop,\n (dimensions.height - contentHeight - insetBottom) / 2,\n );\n top = centeredPosition;\n }\n }\n }\n\n return { top, maxHeight: maxContentHeight, maxWidth: maxContentWidth };\n}\n\nfunction getHorizontalAlignPosition(\n align: \"start\" | \"center\" | \"end\",\n triggerPageX: number,\n triggerWidth: number,\n contentWidth: number,\n alignOffset: number,\n insetLeft: number,\n insetRight: number,\n dimensions: ScaledSize,\n) {\n let left = 0;\n if (align === \"start\") {\n left = triggerPageX;\n }\n if (align === \"center\") {\n left = triggerPageX + triggerWidth / 2 - contentWidth / 2;\n }\n if (align === \"end\") {\n left = triggerPageX + triggerWidth - contentWidth;\n }\n return Math.max(\n insetLeft,\n Math.min(left + alignOffset, dimensions.width - contentWidth - insetRight),\n );\n}\n\nfunction getVerticalAlignPosition(\n align: \"start\" | \"center\" | \"end\",\n triggerPageY: number,\n triggerHeight: number,\n contentHeight: number,\n alignOffset: number,\n insetTop: number,\n insetBottom: number,\n dimensions: ScaledSize,\n) {\n let top = 0;\n if (align === \"start\") {\n top = triggerPageY;\n }\n if (align === \"center\") {\n top = triggerPageY + triggerHeight / 2 - contentHeight / 2;\n }\n if (align === \"end\") {\n top = triggerPageY + triggerHeight - contentHeight;\n }\n return Math.max(\n insetTop,\n Math.min(top + alignOffset, dimensions.height - contentHeight - insetBottom),\n );\n}\n\ntype GetContentStyleArgs = GetPositionArgs & GetSidePositionArgs & GetAlignPositionArgs;\n\nfunction getContentStyle({\n align,\n avoidCollisions,\n contentLayout,\n placement,\n triggerPosition,\n alignOffset,\n insets,\n offset,\n dimensions,\n}: GetContentStyleArgs) {\n return Object.assign(\n { position: \"absolute\" } as const,\n getSidePosition({\n placement,\n triggerPosition,\n contentLayout,\n offset,\n insets,\n avoidCollisions,\n dimensions,\n }),\n getAlignPosition({\n align,\n avoidCollisions,\n triggerPosition,\n contentLayout,\n alignOffset,\n insets,\n dimensions,\n placement,\n }),\n );\n}\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/hooks/use-relative-position.ts" }, { "path": "registry/native-ui/src/helpers/internal/types/animation.ts", "content": "import type {\n BaseAnimationBuilder,\n EntryOrExitLayoutType,\n LayoutAnimationFunction,\n WithSpringConfig,\n WithTimingConfig,\n} from \"react-native-reanimated\";\n\n/**\n * Universal animation prop type\n * - `true` or `undefined`: Use default animations\n * - `false` or `\"disabled\"`: Disable all animations\n * - `object`: Custom animation configuration\n * - Can include `state?: 'disabled' | boolean | undefined` to disable animations while customizing properties\n */\nexport type Animation = Record> =\n | boolean\n | \"disabled\"\n | (TConfig & { state?: \"disabled\" | boolean });\n\nexport type AnimationDisabled = \"disabled\" | false;\n\n/**\n * Root-level animation prop type with cascading control\n * - `true` or `undefined`: Use default animations\n * - `false` or `\"disabled\"`: Disable only root animations (children can still animate)\n * - `\"disable-all\"`: Disable all animations including children (cascades down)\n * - `object`: Custom animation configuration\n * - Can include `state?: 'disabled' | 'disable-all' | boolean` to disable animations while customizing properties\n */\nexport type AnimationRoot = Record> =\n | boolean\n | \"disabled\"\n | \"disable-all\"\n | (TConfig & { state?: \"disabled\" | \"disable-all\" | boolean });\n\nexport type AnimationRootDisableAll = Extract;\n\n/**\n * Animation value that can be a custom config\n * Used for granular animation control within a component\n */\nexport type AnimationValue = Record> = TConfig;\n\nexport type LayoutTransition =\n | BaseAnimationBuilder\n | LayoutAnimationFunction\n | typeof BaseAnimationBuilder\n | undefined;\n\n/**\n * Spring animation configuration\n */\nexport interface SpringAnimationConfig {\n type: \"spring\";\n config?: WithSpringConfig;\n}\n\n/**\n * Timing animation configuration\n */\nexport interface TimingAnimationConfig {\n type: \"timing\";\n config?: WithTimingConfig;\n}\n\n/**\n * Animation configuration for popup overlay components (Dialog, Select, BottomSheet, Popover, etc.)\n * Supports both progress-based opacity animation and entering/exiting animations\n */\nexport type PopupOverlayAnimation = Animation<{\n /**\n * Opacity animation configuration (progress-based)\n * Takes effect for bottom-sheet/dialog presentation\n * @default [0, 1, 0] - opacity values for [idle, open, close] states\n */\n opacity?: AnimationValue<{\n /**\n * Opacity values [idle, open, close]\n * @default [0, 1, 0]\n */\n value?: [number, number, number];\n }>;\n /**\n * Takes effect for popover presentation\n * @default FadeIn with duration 200ms\n */\n entering?: EntryOrExitLayoutType;\n /**\n * Takes effect for popover presentation\n * @default FadeOut with duration 150ms\n */\n exiting?: EntryOrExitLayoutType;\n}>;\n\n/**\n * Animation configuration for popup dialog content components (Dialog, Select dialog presentation)\n * Supports opacity and scale animations\n */\nexport type PopupDialogContentAnimation = Animation<{\n /**\n * Custom Keyframe animation for entering transition\n * @default Keyframe with scale, and opacity (200ms)\n */\n entering?: EntryOrExitLayoutType;\n /**\n * Custom Keyframe animation for exiting transition\n * @default Keyframe mirroring entering animation (150ms)\n */\n exiting?: EntryOrExitLayoutType;\n}>;\n\n/**\n * Animation configuration for popup popover content components (Popover, Select popover presentation)\n * Supports custom Keyframe animations for entering and exiting transitions\n */\nexport type PopupPopoverContentAnimation = Animation<{\n /**\n * Custom Keyframe animation for entering transition\n * @default Keyframe with translateY/translateX, scale, and opacity (200ms)\n */\n entering?: EntryOrExitLayoutType;\n /**\n * Custom Keyframe animation for exiting transition\n * @default Keyframe mirroring entering animation (150ms)\n */\n exiting?: EntryOrExitLayoutType;\n}>;\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/types/animation.ts" }, { "path": "registry/native-ui/src/helpers/internal/types/bottom-sheet.ts", "content": "import type { BottomSheetViewProps } from \"@gorhom/bottom-sheet/lib/typescript/components/bottomSheetView/types\";\nimport type { ReactNode } from \"react\";\nimport type { SharedValue } from \"react-native-reanimated\";\nimport type { AnimationDisabled } from \"./animation\";\n\n/**\n * State type for bottom sheet content container animation coordination\n */\nexport type BottomSheetContentContainerState = \"idle\" | \"open\" | \"close\";\n\n/**\n * Props for the reusable BottomSheetContentContainer component\n */\nexport interface BottomSheetContentContainerProps {\n /**\n * The content to be rendered inside the container\n */\n children?: ReactNode;\n /**\n * Additional CSS class for the content container\n */\n contentContainerClassName?: string;\n /**\n * Props for the content container\n */\n contentContainerProps?: Omit;\n /**\n * Whether the bottom sheet is open\n */\n isOpen: boolean;\n /**\n * Animation progress shared value (0=idle, 1=open, 2=close)\n */\n progress: SharedValue;\n /**\n * Whether the bottom sheet is dragging\n */\n isDragging: SharedValue;\n /**\n * Whether the bottom sheet is pan activated\n */\n isPanActivated: SharedValue;\n /**\n * Whether the bottom sheet is closing on swipe\n */\n isClosingOnSwipe: SharedValue;\n /**\n * Initial index of the bottom sheet\n */\n initialIndex: number;\n /**\n * Callback when the bottom sheet is opened\n */\n onOpenChange: (open: boolean) => void;\n /**\n * Whether the bottom sheet can be closed by panning down\n */\n enablePanDownToClose: boolean;\n}\n\n/**\n * Base props shared across BottomSheet Content components\n * Used by BottomSheet, Popover, and Select components when using bottom-sheet presentation\n */\nexport interface BaseBottomSheetContentProps {\n /**\n * The bottom sheet content\n */\n children?: ReactNode;\n /**\n * Additional CSS class for the bottom sheet\n */\n className?: string;\n /**\n * Additional CSS class for the container\n */\n containerClassName?: string;\n /**\n * Additional CSS class for the content container\n */\n contentContainerClassName?: string;\n /**\n * Additional CSS class for the background\n */\n backgroundClassName?: string;\n /**\n * Additional CSS class for the handle\n */\n handleClassName?: string;\n /**\n * Additional CSS class for the handle indicator\n */\n handleIndicatorClassName?: string;\n /**\n * Props for the content container\n */\n contentContainerProps?: Omit;\n /**\n * Animation configuration for bottom sheet content\n * - `false` or `\"disabled\"`: Disable all animations\n */\n animation?: AnimationDisabled;\n}\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/types/bottom-sheet.ts" }, { "path": "registry/native-ui/src/helpers/internal/types/index.ts", "content": "export * from \"./animation\";\nexport * from \"./bottom-sheet\";\nexport * from \"./misc\";\nexport * from \"./primitives\";\nexport * from \"./theme\";\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/types/index.ts" }, { "path": "registry/native-ui/src/helpers/internal/types/misc.ts", "content": "type ReactChild =\n | string\n | number\n | bigint\n | React.ReactElement>\n | Iterable\n | React.ReactPortal\n | Promise;\n\nexport type { ReactChild };\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/types/misc.ts" }, { "path": "registry/native-ui/src/helpers/internal/types/primitives.ts", "content": "import type { Pressable, Text, View, ViewStyle } from \"react-native\";\n\n// Base utility types\n\n/**\n * Component props with optional asChild prop for polymorphic components\n * Allows components to render as different elements when asChild is true\n */\ntype ComponentPropsWithAsChild> =\n React.ComponentPropsWithoutRef & { asChild?: boolean };\n\n// Ref types\n\n/**\n * Reference type for React Native View component\n * Used for forwarding refs to View elements\n */\ntype ViewRef = React.ComponentRef;\n\n/**\n * Reference type for React Native Pressable component\n * Used for forwarding refs to Pressable elements\n */\ntype PressableRef = React.ComponentRef;\n\n/**\n * Reference type for React Native Text component\n * Used for forwarding refs to Text elements\n */\ntype TextRef = React.ComponentRef;\n\n// Slottable component props\n\n/**\n * View component props with asChild support for slot composition\n * Enables View components to be used with the Slot pattern\n */\ntype SlottableViewProps = ComponentPropsWithAsChild;\n\n/**\n * Pressable component props with asChild support for slot composition\n * Enables Pressable components to be used with the Slot pattern\n */\ntype SlottablePressableProps = ComponentPropsWithAsChild;\n\n/**\n * Text component props with asChild support for slot composition\n * Enables Text components to be used with the Slot pattern\n */\ntype SlottableTextProps = ComponentPropsWithAsChild;\n\n/**\n * Interface for components that can be force mounted even when normally hidden\n */\ninterface ForceMountable {\n /**\n * Whether to force mount the component in the DOM\n * Useful for animation purposes when component needs to be present but hidden\n */\n forceMount?: true | undefined;\n}\n\n/**\n * Interface for defining spacing/padding from screen edges\n */\ninterface Insets {\n /**\n * Distance from the top edge in pixels\n */\n top?: number;\n /**\n * Distance from the bottom edge in pixels\n */\n bottom?: number;\n /**\n * Distance from the left edge in pixels\n */\n left?: number;\n /**\n * Distance from the right edge in pixels\n */\n right?: number;\n}\n\n/**\n * Props for components that need to be positioned relative to a trigger element\n * Certain props are only available on the native version of the component.\n * @docs For the web version, see the Radix documentation https://www.radix-ui.com/primitives\n */\ninterface PositionedContentProps {\n /**\n * Whether to force mount the component in the DOM\n */\n forceMount?: true | undefined;\n /**\n * Custom styles to apply to the positioned content\n */\n style?: ViewStyle;\n /**\n * Offset along the alignment axis in pixels\n */\n alignOffset?: number;\n /**\n * Screen edge insets to respect when positioning\n */\n insets?: Insets;\n /**\n * Whether to automatically adjust position to avoid screen edges\n * @default true\n */\n avoidCollisions?: boolean;\n /**\n * Alignment relative to the trigger element\n * @default 'start'\n */\n align?: \"start\" | \"center\" | \"end\";\n /**\n * Preferred placement of the trigger element to position against\n * @default 'bottom'\n */\n placement?: \"top\" | \"bottom\" | \"left\" | \"right\";\n /**\n * Offset from the trigger element in pixels\n * @default 0\n */\n offset?: number;\n /**\n * Whether to disable the automatic positioning styles\n * Useful when you want to handle positioning manually\n * @default false\n */\n disablePositioningStyle?: boolean;\n}\n\nexport type {\n ComponentPropsWithAsChild,\n ForceMountable,\n Insets,\n PositionedContentProps,\n PressableRef,\n SlottablePressableProps,\n SlottableTextProps,\n SlottableViewProps,\n TextRef,\n ViewRef,\n};\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/types/primitives.ts" }, { "path": "registry/native-ui/src/helpers/internal/types/theme.ts", "content": "import type { ClassValue } from \"tailwind-variants\";\n\n/**\n * This Typescript utility transform a list of slots into a list of {slot: classes}\n */\ntype ElementSlots = {\n [key in S]?: Exclude;\n};\n\n/**\n * Type helper that preserves the exact type of combined style objects\n * This ensures that VariantProps inference works correctly for each style\n */\ntype CombinedStyles> = {\n [K in keyof T]: T[K];\n};\n\nexport type { CombinedStyles, ElementSlots };\n", "type": "registry:hook", "target": "@components/pitsi-ui/native-ui/src/helpers/internal/types/theme.ts" } ], "categories": [ "native", "react-native", "internal-hooks" ], "meta": { "package": "@pitsi-ui/native", "packageSlug": "native-ui", "platform": "native" } }