@butternutbox/pawprint-native 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -1,14 +1,14 @@
1
- import React, { useEffect, useRef, useState } from "react"
1
+ import React from "react"
2
2
  import { View, StyleSheet } from "react-native"
3
3
  import { Menu as MenuIcon } from "@butternutbox/pawprint-icons/core"
4
4
  import { Menu } from "./Menu"
5
+ import { useMenuTrigger } from "./hooks"
5
6
  import { IconButton } from "../../atoms/IconButton"
6
7
 
7
8
  export default {
8
9
  title: "Molecules/Menu",
9
10
  component: Menu,
10
11
  argTypes: {
11
- open: { control: "boolean", description: "Whether the menu is visible" },
12
12
  align: {
13
13
  control: "select",
14
14
  options: ["left", "right"],
@@ -28,58 +28,31 @@ const styles = StyleSheet.create({
28
28
  }
29
29
  })
30
30
 
31
- // `anchorOffset` is a *window* coordinate — the distance from the top of the
32
- // screen to the bar's bottom edge. In a real app the header sits at the top of
33
- // the screen, so this is roughly the bar's height. In Storybook the bar sits
34
- // lower in the canvas, so we measure its actual on-screen position; otherwise
35
- // the card drops from the wrong Y and looks offset from the bar.
36
- const useBarAnchor = () => {
37
- const ref = useRef<View>(null)
38
- const [anchorOffset, setAnchorOffset] = useState(0)
39
-
40
- const measure = () =>
41
- ref.current?.measureInWindow((_x, y, _w, height) =>
42
- setAnchorOffset(y + height)
43
- )
44
-
45
- return { ref, anchorOffset, measure }
46
- }
47
-
48
31
  // --- Playground ---
49
- // The `open` control seeds the initial state; the burger reopens it and an
50
- // outside tap (or the OS back gesture) dismisses via `onClose`. Without local
51
- // state the menu would be stuck open, since the RN Modal overlays Storybook's
52
- // own controls panel and blocks you from flipping the `open` toggle back off.
32
+ // Tap the burger to open; an outside tap (or the OS back gesture) dismisses via
33
+ // `onClose`. The menu is driven by local state rather than an `open` control
34
+ // because the RN Modal overlays Storybook's controls panel, which would leave
35
+ // it stuck open.
53
36
 
54
37
  export const Playground = {
55
38
  args: {
56
- open: true,
57
39
  align: "right",
58
- width: 360
40
+ width: 280
59
41
  },
60
42
  render: (args: React.ComponentProps<typeof Menu>) => {
61
- const [open, setOpen] = useState(args.open)
62
- const { ref, anchorOffset, measure } = useBarAnchor()
63
-
64
- // Re-sync when the `open` control is toggled in the Storybook panel.
65
- useEffect(() => setOpen(args.open), [args.open])
43
+ const { triggerRef, open, menuProps } = useMenuTrigger()
66
44
 
67
45
  return (
68
- <View>
69
- <View ref={ref} style={styles.bar} onLayout={measure}>
46
+ <View style={styles.bar}>
47
+ <View ref={triggerRef} collapsable={false}>
70
48
  <IconButton
71
49
  icon={MenuIcon}
72
50
  variant="text"
73
51
  aria-label="Open menu"
74
- onPress={() => setOpen(true)}
52
+ onPress={open}
75
53
  />
76
54
  </View>
77
- <Menu
78
- {...args}
79
- open={open}
80
- onClose={() => setOpen(false)}
81
- anchorOffset={anchorOffset}
82
- >
55
+ <Menu {...args} {...menuProps}>
83
56
  <Menu.Item onPress={() => {}}>Option 1</Menu.Item>
84
57
  <Menu.Item onPress={() => {}}>Option 2</Menu.Item>
85
58
  <Menu.Item variant="destructive" onPress={() => {}}>
@@ -99,30 +72,21 @@ export const Playground = {
99
72
  export const Interactive = {
100
73
  name: "Interactive / Header trigger",
101
74
  render: () => {
102
- const [open, setOpen] = useState(false)
103
- const { ref, anchorOffset, measure } = useBarAnchor()
75
+ const { triggerRef, open, close, menuProps } = useMenuTrigger()
104
76
 
105
77
  return (
106
- <View>
107
- <View ref={ref} style={styles.bar} onLayout={measure}>
78
+ <View style={styles.bar}>
79
+ <View ref={triggerRef} collapsable={false}>
108
80
  <IconButton
109
81
  icon={MenuIcon}
110
82
  variant="text"
111
83
  aria-label="Open menu"
112
- onPress={() => setOpen(true)}
84
+ onPress={open}
113
85
  />
114
86
  </View>
115
- <Menu
116
- open={open}
117
- onClose={() => setOpen(false)}
118
- anchorOffset={anchorOffset}
119
- >
120
- <Menu.Item onPress={() => setOpen(false)}>Option 1</Menu.Item>
121
- <Menu.Item
122
- variant="destructive"
123
- divider={false}
124
- onPress={() => setOpen(false)}
125
- >
87
+ <Menu {...menuProps}>
88
+ <Menu.Item onPress={close}>Option 1</Menu.Item>
89
+ <Menu.Item variant="destructive" divider={false} onPress={close}>
126
90
  Option 2
127
91
  </Menu.Item>
128
92
  </Menu>
@@ -1,4 +1,5 @@
1
1
  import React from "react"
2
+ import { Dimensions } from "react-native"
2
3
  import { screen, fireEvent } from "@testing-library/react"
3
4
  import { describe, it, expect, vi } from "vitest"
4
5
  import { renderWithTheme } from "../../../test-utils"
@@ -15,7 +16,11 @@ const BasicMenu = ({
15
16
  onDiscounts?: () => void
16
17
  onLogOut?: () => void
17
18
  }) => (
18
- <Menu open={open} onClose={onClose} anchorOffset={80}>
19
+ <Menu
20
+ open={open}
21
+ onClose={onClose}
22
+ anchor={{ x: 200, y: 20, width: 120, height: 40 }}
23
+ >
19
24
  <Menu.Item onPress={onDiscounts}>Discount codes</Menu.Item>
20
25
  <Menu.Item variant="destructive" onPress={onLogOut}>
21
26
  Log out
@@ -36,12 +41,61 @@ describe("Menu", () => {
36
41
  expect(screen.getByText("Log out")).toBeInTheDocument()
37
42
  })
38
43
 
44
+ it("still renders with the deprecated anchorOffset", () => {
45
+ renderWithTheme(
46
+ <Menu open onClose={() => {}} anchorOffset={80}>
47
+ <Menu.Item onPress={() => {}}>Discount codes</Menu.Item>
48
+ </Menu>
49
+ )
50
+ expect(screen.getByText("Discount codes")).toBeInTheDocument()
51
+ })
52
+
39
53
  it("exposes each row as a menuitem", () => {
40
54
  renderWithTheme(<BasicMenu />)
41
55
  expect(screen.getAllByRole("menuitem")).toHaveLength(2)
42
56
  })
43
57
  })
44
58
 
59
+ describe("positioning", () => {
60
+ it("hangs the card below the trigger, right edges aligned", () => {
61
+ renderWithTheme(<BasicMenu />)
62
+ const anchor = screen.getByRole("menu").parentElement!
63
+ expect(anchor.style.top).toBe("68px")
64
+ expect(anchor.style.right).toBe(
65
+ `${Dimensions.get("window").width - 320}px`
66
+ )
67
+ })
68
+
69
+ it("keeps its position while closing after the anchor is cleared", () => {
70
+ const anchor = { x: 200, y: 20, width: 120, height: 40 }
71
+ const { rerender } = renderWithTheme(
72
+ <Menu open onClose={() => {}} anchor={anchor}>
73
+ <Menu.Item onPress={() => {}}>Discount codes</Menu.Item>
74
+ </Menu>
75
+ )
76
+ rerender(
77
+ <Menu open onClose={() => {}}>
78
+ <Menu.Item onPress={() => {}}>Discount codes</Menu.Item>
79
+ </Menu>
80
+ )
81
+ expect(screen.getByRole("menu").parentElement!.style.top).toBe("68px")
82
+ })
83
+
84
+ it("lines up left edges when aligned left", () => {
85
+ renderWithTheme(
86
+ <Menu
87
+ open
88
+ onClose={() => {}}
89
+ align="left"
90
+ anchor={{ x: 24, y: 20, width: 160, height: 40 }}
91
+ >
92
+ <Menu.Item onPress={() => {}}>Discount codes</Menu.Item>
93
+ </Menu>
94
+ )
95
+ expect(screen.getByRole("menu").parentElement!.style.left).toBe("24px")
96
+ })
97
+ })
98
+
45
99
  describe("interaction", () => {
46
100
  it("fires an item's onPress when tapped", () => {
47
101
  const onLogOut = vi.fn()
@@ -1,26 +1,41 @@
1
- import React from "react"
2
- import { Modal, Pressable, View } from "react-native"
1
+ import React, { useState } from "react"
2
+ import { Modal, Pressable, View, useWindowDimensions } from "react-native"
3
3
  import styled from "@emotion/native"
4
+ import { useTheme } from "@emotion/react"
4
5
  import { MenuItem } from "./MenuItem"
5
6
 
6
7
  const parseTokenValue = (value: string): number => parseFloat(value)
7
8
 
8
9
  export type MenuAlign = "left" | "right"
9
10
 
11
+ /** A rect in window coordinates, as returned by `View#measureInWindow`. */
12
+ export type MenuAnchorRect = {
13
+ x: number
14
+ y: number
15
+ width: number
16
+ height: number
17
+ }
18
+
10
19
  export type MenuProps = {
11
20
  /** Whether the menu is visible. */
12
21
  open: boolean
13
22
  /** Called when the user taps outside the card or dismisses via the OS. */
14
23
  onClose: () => void
15
24
  /**
16
- * Distance, in px, from the top of the screen to where the menu drops from —
17
- * typically the bottom edge of the bar/header the menu hangs beneath, so the
18
- * card opens flush below it.
25
+ * The trigger's rect in window coordinates (from `measureInWindow`). The
26
+ * card drops just below it, with its `align` edge lined up with the
27
+ * trigger's.
28
+ */
29
+ anchor?: MenuAnchorRect
30
+ /**
31
+ * @deprecated Pass `anchor` so the card hangs from the trigger. Distance, in
32
+ * px, from the top of the screen to the bottom edge of the bar the card drops
33
+ * flush beneath, inset from the screen edge. Ignored when `anchor` is set.
19
34
  */
20
35
  anchorOffset?: number
21
36
  /** Which edge the card aligns to. Defaults to `right`. */
22
37
  align?: MenuAlign
23
- /** Card width in px. Defaults to 360 (capped at 90% of the screen). */
38
+ /** Card width in px. Defaults to 280 (capped at 90% of the window). */
24
39
  width?: number
25
40
  /** Accessibility label for the outside-tap dismiss layer. */
26
41
  closeAccessibilityLabel?: string
@@ -40,15 +55,8 @@ const DismissLayer = styled(Pressable)({
40
55
  bottom: 0
41
56
  })
42
57
 
43
- // Positions the card against the aligned edge, flush beneath the anchor
44
- // (`top` set inline to `anchorOffset`).
45
- const Anchor = styled(View)<{ align: MenuAlign }>(({ theme, align }) => {
46
- const inset = parseTokenValue(theme.tokens.semantics.dimensions.spacing.md)
47
-
48
- return {
49
- position: "absolute",
50
- ...(align === "right" ? { right: inset } : { left: inset })
51
- }
58
+ const Anchor = styled(View)({
59
+ position: "absolute"
52
60
  })
53
61
 
54
62
  // A Pressable (rather than a View) so taps on the card's own chrome are
@@ -59,7 +67,6 @@ const Card = styled(Pressable)<{ width: number }>(({ theme, width }) => {
59
67
 
60
68
  return {
61
69
  width,
62
- maxWidth: "90%",
63
70
  borderRadius: parseTokenValue(borderRadius.md),
64
71
  overflow: "hidden",
65
72
  backgroundColor: background.surface.default,
@@ -78,12 +85,33 @@ const Card = styled(Pressable)<{ width: number }>(({ theme, width }) => {
78
85
  const MenuRoot = ({
79
86
  open,
80
87
  onClose,
88
+ anchor,
81
89
  anchorOffset = 0,
82
90
  align = "right",
83
- width = 360,
91
+ width = 280,
84
92
  closeAccessibilityLabel = "Close menu",
85
93
  children
86
94
  }: MenuProps) => {
95
+ const theme = useTheme()
96
+ const { width: windowWidth } = useWindowDimensions()
97
+ const { spacing } = theme.tokens.semantics.dimensions
98
+
99
+ const [lastAnchor, setLastAnchor] = useState(anchor)
100
+ if (anchor && anchor !== lastAnchor) setLastAnchor(anchor)
101
+ const shownAnchor = anchor ?? lastAnchor
102
+
103
+ const position = shownAnchor
104
+ ? {
105
+ top: shownAnchor.y + shownAnchor.height + parseTokenValue(spacing.xs),
106
+ ...(align === "right"
107
+ ? { right: windowWidth - (shownAnchor.x + shownAnchor.width) }
108
+ : { left: shownAnchor.x })
109
+ }
110
+ : {
111
+ top: anchorOffset,
112
+ [align]: parseTokenValue(spacing.md)
113
+ }
114
+
87
115
  return (
88
116
  <Modal
89
117
  visible={open}
@@ -97,9 +125,9 @@ const MenuRoot = ({
97
125
  accessible={false}
98
126
  accessibilityLabel={closeAccessibilityLabel}
99
127
  />
100
- <Anchor align={align} style={{ top: anchorOffset }}>
128
+ <Anchor style={position}>
101
129
  <Card
102
- width={width}
130
+ width={Math.min(width, windowWidth * 0.9)}
103
131
  onPress={() => {}}
104
132
  accessible={false}
105
133
  accessibilityRole="menu"
@@ -114,23 +142,20 @@ const MenuRoot = ({
114
142
  MenuRoot.displayName = "Menu"
115
143
 
116
144
  /**
117
- * A dropdown menu that expands beneath an anchor (typically a header/bar).
118
- * Tapping outside the card dismisses it; the page behind is left undarkened.
119
- * The consumer owns open/close state and supplies the rows as `Menu.Item`
120
- * children; the app-specific content stays at the call site.
145
+ * A dropdown menu that hangs beneath its trigger. Tapping outside the card
146
+ * dismisses it; the page behind is left undarkened. The consumer owns
147
+ * open/close state and supplies the rows as `Menu.Item` children; the
148
+ * app-specific content stays at the call site.
121
149
  *
122
- * Pass `anchorOffset` (the anchor bar's bottom edge in window coordinates) so
123
- * the card drops just below it.
150
+ * `useMenuTrigger` measures the trigger and supplies the `anchor`, open state
151
+ * and `onClose` as `menuProps`.
124
152
  *
125
153
  * @example
126
154
  * ```tsx
127
- * const [open, setOpen] = useState(false)
128
- * const [barHeight, setBarHeight] = useState(0)
155
+ * const { triggerRef, open, menuProps } = useMenuTrigger()
129
156
  *
130
- * <View onLayout={(e) => setBarHeight(e.nativeEvent.layout.height)}>
131
- * <Pressable onPress={() => setOpen(true)}>...</Pressable>
132
- * </View>
133
- * <Menu open={open} onClose={() => setOpen(false)} anchorOffset={barHeight}>
157
+ * <Pressable ref={triggerRef} onPress={open}>...</Pressable>
158
+ * <Menu {...menuProps}>
134
159
  * <Menu.Item onPress={onDiscounts}>Discount codes</Menu.Item>
135
160
  * <Menu.Item variant="destructive" divider={false} onPress={onLogOut}>
136
161
  * Log out
@@ -2,6 +2,7 @@ import React from "react"
2
2
  import { Pressable, type PressableProps } from "react-native"
3
3
  import styled from "@emotion/native"
4
4
  import { useTheme } from "@emotion/react"
5
+ import { Divider } from "../../atoms/Divider"
5
6
  import { Typography } from "../../atoms/Typography"
6
7
 
7
8
  const parseTokenValue = (value: string): number => parseFloat(value)
@@ -10,8 +11,8 @@ export type MenuItemVariant = "action" | "destructive"
10
11
 
11
12
  type MenuItemOwnProps = {
12
13
  /**
13
- * `action` renders the label in the brand action colour, `destructive` in
14
- * the error colour (e.g. "Log out"). Defaults to `action`.
14
+ * `action` renders the label in the link colour, `destructive` in the error
15
+ * colour (e.g. "Log out"). Defaults to `action`.
15
16
  */
16
17
  variant?: MenuItemVariant
17
18
  onPress: () => void
@@ -19,7 +20,7 @@ type MenuItemOwnProps = {
19
20
  children: React.ReactNode
20
21
  /**
21
22
  * Whether to render a divider beneath the row. Defaults to `true`; set
22
- * `false` on the last row so the border doesn't sit against the card's
23
+ * `false` on the last row so the divider doesn't sit against the card's
23
24
  * rounded bottom edge.
24
25
  */
25
26
  divider?: boolean
@@ -28,27 +29,21 @@ type MenuItemOwnProps = {
28
29
  export type MenuItemProps = MenuItemOwnProps &
29
30
  Omit<PressableProps, keyof MenuItemOwnProps | "children">
30
31
 
31
- // A full-width, centre-aligned row. `divider` paints the bottom border; set it
32
- // `false` on the last row so the border doesn't sit against the card's rounded
33
- // bottom edge.
34
- const Row = styled(Pressable)<{ divider: boolean }>(({ theme, divider }) => {
32
+ const Row = styled(Pressable)(({ theme }) => {
35
33
  const { spacing } = theme.tokens.semantics.dimensions
36
- const { colour } = theme.tokens.semantics
37
34
 
38
35
  return {
39
36
  width: "100%",
40
- alignItems: "center",
37
+ alignItems: "flex-start",
41
38
  justifyContent: "center",
42
39
  paddingVertical: parseTokenValue(spacing.md),
43
- paddingHorizontal: parseTokenValue(spacing.md),
44
- borderBottomWidth: divider ? 1 : 0,
45
- borderBottomColor: colour.border.divider
40
+ paddingHorizontal: parseTokenValue(spacing.md)
46
41
  }
47
42
  })
48
43
 
49
44
  /**
50
- * A single row inside a {@link Menu}. Renders its label centred, coloured by
51
- * `variant`, with a divider beneath it unless `divider` is set to `false`.
45
+ * A single row inside a {@link Menu}. Renders its label left-aligned, coloured
46
+ * by `variant`, with a divider beneath it unless `divider` is set to `false`.
52
47
  *
53
48
  * @example
54
49
  * ```tsx
@@ -64,11 +59,11 @@ export const MenuItem = React.forwardRef<
64
59
  const theme = useTheme()
65
60
  const { colour, typography } = theme.tokens.semantics
66
61
 
67
- // The action colour lives under `colour.text.action`, which Typography's
68
- // semantic-key `color` prop deliberately excludes — so we render in token
69
- // mode (font token + hex colour). Both colours still come from tokens.
62
+ // The link colour lives under `colour.text.link`, which Typography's
63
+ // semantic-key `color` prop excludes — so we render in token mode (font
64
+ // token + hex colour). Both colours still come from tokens.
70
65
  const labelColour =
71
- variant === "destructive" ? colour.text.error : colour.text.action.default
66
+ variant === "destructive" ? colour.text.error : colour.text.link.default
72
67
 
73
68
  const label =
74
69
  typeof children === "string" || typeof children === "number"
@@ -76,27 +71,25 @@ export const MenuItem = React.forwardRef<
76
71
  : undefined
77
72
 
78
73
  return (
79
- <Row
80
- ref={ref}
81
- divider={divider}
82
- onPress={onPress}
83
- accessible
84
- accessibilityRole="menuitem"
85
- accessibilityLabel={label}
86
- {...rest}
87
- >
88
- {label !== undefined ? (
89
- <Typography
90
- token={typography.body.medium.md}
91
- align="center"
92
- color={labelColour}
93
- >
94
- {label}
95
- </Typography>
96
- ) : (
97
- children
98
- )}
99
- </Row>
74
+ <>
75
+ <Row
76
+ ref={ref}
77
+ onPress={onPress}
78
+ accessible
79
+ accessibilityRole="menuitem"
80
+ accessibilityLabel={label}
81
+ {...rest}
82
+ >
83
+ {label !== undefined ? (
84
+ <Typography token={typography.body.medium.md} color={labelColour}>
85
+ {label}
86
+ </Typography>
87
+ ) : (
88
+ children
89
+ )}
90
+ </Row>
91
+ {divider && <Divider />}
92
+ </>
100
93
  )
101
94
  })
102
95
 
@@ -0,0 +1 @@
1
+ export { useMenuTrigger } from "./useMenuTrigger"
@@ -0,0 +1,48 @@
1
+ import { act, renderHook } from "@testing-library/react"
2
+ import type { View } from "react-native"
3
+ import { describe, expect, it } from "vitest"
4
+ import { useMenuTrigger } from "./useMenuTrigger"
5
+
6
+ const measurable = { x: 200, y: 20, width: 120, height: 40 }
7
+
8
+ const renderTrigger = () => {
9
+ const hook = renderHook(() => useMenuTrigger())
10
+ hook.result.current.triggerRef.current = {
11
+ measureInWindow: (
12
+ callback: (x: number, y: number, width: number, height: number) => void
13
+ ) =>
14
+ callback(measurable.x, measurable.y, measurable.width, measurable.height)
15
+ } as unknown as View
16
+ return hook
17
+ }
18
+
19
+ describe("useMenuTrigger", () => {
20
+ it("starts closed", () => {
21
+ const { result } = renderTrigger()
22
+ expect(result.current.isOpen).toBe(false)
23
+ expect(result.current.menuProps).toEqual({
24
+ open: false,
25
+ onClose: result.current.close,
26
+ anchor: undefined
27
+ })
28
+ })
29
+
30
+ it("opens anchored to the measured trigger", () => {
31
+ const { result } = renderTrigger()
32
+ act(() => result.current.open())
33
+ expect(result.current.isOpen).toBe(true)
34
+ expect(result.current.menuProps.open).toBe(true)
35
+ expect(result.current.menuProps.anchor).toEqual(measurable)
36
+ })
37
+
38
+ it("closes from close and from the menu's onClose", () => {
39
+ const { result } = renderTrigger()
40
+ act(() => result.current.open())
41
+ act(() => result.current.close())
42
+ expect(result.current.isOpen).toBe(false)
43
+
44
+ act(() => result.current.open())
45
+ act(() => result.current.menuProps.onClose())
46
+ expect(result.current.isOpen).toBe(false)
47
+ })
48
+ })
@@ -0,0 +1,40 @@
1
+ import { useCallback, useRef, useState } from "react"
2
+ import type { View } from "react-native"
3
+ import type { MenuAnchorRect } from "../Menu"
4
+
5
+ /**
6
+ * Wires a trigger to a {@link Menu}: measures the trigger when it opens so the
7
+ * card hangs beneath it.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const { triggerRef, open, close, menuProps } = useMenuTrigger()
12
+ *
13
+ * <Pressable ref={triggerRef} onPress={open}>...</Pressable>
14
+ * <Menu {...menuProps}>
15
+ * <Menu.Item onPress={close}>Discount codes</Menu.Item>
16
+ * </Menu>
17
+ * ```
18
+ */
19
+ export const useMenuTrigger = () => {
20
+ const triggerRef = useRef<View>(null)
21
+ const [anchor, setAnchor] = useState<MenuAnchorRect>()
22
+
23
+ const open = useCallback(() => {
24
+ triggerRef.current?.measureInWindow((x, y, width, height) =>
25
+ setAnchor({ x, y, width, height })
26
+ )
27
+ }, [])
28
+
29
+ const close = useCallback(() => setAnchor(undefined), [])
30
+
31
+ const isOpen = anchor !== undefined
32
+
33
+ return {
34
+ triggerRef,
35
+ isOpen,
36
+ open,
37
+ close,
38
+ menuProps: { open: isOpen, onClose: close, anchor }
39
+ }
40
+ }
@@ -1,4 +1,5 @@
1
1
  export { Menu } from "./Menu"
2
- export type { MenuProps, MenuAlign } from "./Menu"
2
+ export type { MenuProps, MenuAlign, MenuAnchorRect } from "./Menu"
3
3
  export { MenuItem } from "./MenuItem"
4
4
  export type { MenuItemProps, MenuItemVariant } from "./MenuItem"
5
+ export { useMenuTrigger } from "./hooks"