@ankorar/nodex 0.0.1

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.
Files changed (62) hide show
  1. package/README.md +228 -0
  2. package/package.json +54 -0
  3. package/src/components/mindMap/Background.tsx +39 -0
  4. package/src/components/mindMap/Board.tsx +159 -0
  5. package/src/components/mindMap/CentalNode.tsx +121 -0
  6. package/src/components/mindMap/DefaultNode.tsx +205 -0
  7. package/src/components/mindMap/Header.tsx +247 -0
  8. package/src/components/mindMap/ImageNode.tsx +345 -0
  9. package/src/components/mindMap/KeyboardHelpDialog.tsx +108 -0
  10. package/src/components/mindMap/MineMap.tsx +237 -0
  11. package/src/components/mindMap/NodeStylePopover.tsx +486 -0
  12. package/src/components/mindMap/Nodes.tsx +113 -0
  13. package/src/components/mindMap/Nodex.tsx +65 -0
  14. package/src/components/mindMap/SaveStatusIndicator.tsx +61 -0
  15. package/src/components/mindMap/Segments.tsx +270 -0
  16. package/src/components/mindMap/ZenCard.tsx +41 -0
  17. package/src/components/ui/dialog.tsx +141 -0
  18. package/src/components/ui/popover.tsx +46 -0
  19. package/src/components/ui/select.tsx +192 -0
  20. package/src/components/ui/toggle-group.tsx +83 -0
  21. package/src/components/ui/toggle.tsx +45 -0
  22. package/src/config/rootKeyBinds.ts +191 -0
  23. package/src/config/shortCuts.ts +28 -0
  24. package/src/contexts/MindMapNodeEditorContext.tsx +47 -0
  25. package/src/handlers/rootKeyBinds/handleAltEKeyBind.ts +6 -0
  26. package/src/handlers/rootKeyBinds/handleAltHKeyBind.ts +6 -0
  27. package/src/handlers/rootKeyBinds/handleAltWKeyBind.ts +6 -0
  28. package/src/handlers/rootKeyBinds/handleAltZKeyBind.ts +6 -0
  29. package/src/handlers/rootKeyBinds/handleArrowHorizontalRootKeyBind.ts +46 -0
  30. package/src/handlers/rootKeyBinds/handleArrowVerticalRootKeyBind.ts +44 -0
  31. package/src/handlers/rootKeyBinds/handleBackEspaceKeyBind.ts +12 -0
  32. package/src/handlers/rootKeyBinds/handleERootKeyBind.ts +16 -0
  33. package/src/handlers/rootKeyBinds/handleEnterRootKeyBind.ts +35 -0
  34. package/src/handlers/rootKeyBinds/handleEscapeKeyBind.ts +24 -0
  35. package/src/handlers/rootKeyBinds/handleEspaceKeyBind.ts +11 -0
  36. package/src/handlers/rootKeyBinds/handleMoveByWorldKeyBind.ts +6 -0
  37. package/src/handlers/rootKeyBinds/handleRedoRootKeyBind.ts +23 -0
  38. package/src/handlers/rootKeyBinds/handleTabRootKeyBind.ts +49 -0
  39. package/src/handlers/rootKeyBinds/handleTransformNodeKeyBind.ts +39 -0
  40. package/src/handlers/rootKeyBinds/handleUndoRootKeyBind.ts +23 -0
  41. package/src/handlers/rootKeyBinds/handleZoonByKeyBind.ts +31 -0
  42. package/src/helpers/centerNode.ts +19 -0
  43. package/src/helpers/getNodeSide.ts +16 -0
  44. package/src/hooks/mindMap/useHelpers.tsx +9 -0
  45. package/src/hooks/mindMap/useMindMapDebounce.ts +47 -0
  46. package/src/hooks/mindMap/useMindMapHistoryDebounce.ts +69 -0
  47. package/src/hooks/mindMap/useMindMapNode.tsx +203 -0
  48. package/src/hooks/mindMap/useMindMapNodeEditor.ts +91 -0
  49. package/src/hooks/mindMap/useMindMapNodeMouseHandlers.ts +24 -0
  50. package/src/hooks/mindMap/useRootKeyBindHandlers.ts +49 -0
  51. package/src/hooks/mindMap/useRootMouseHandlers.ts +124 -0
  52. package/src/hooks/mindMap/useUpdateCenter.ts +54 -0
  53. package/src/index.ts +76 -0
  54. package/src/lib/utils.ts +6 -0
  55. package/src/state/mindMap.ts +793 -0
  56. package/src/state/mindMapHistory.ts +96 -0
  57. package/src/styles.input.css +95 -0
  58. package/src/utils/exportMindMapAsHighQualityImage.ts +327 -0
  59. package/src/utils/exportMindMapAsMarkdown.ts +102 -0
  60. package/src/utils/exportMindMapAsPdf.ts +241 -0
  61. package/src/utils/getMindMapPreviewDataUrl.ts +60 -0
  62. package/styles.css +2 -0
@@ -0,0 +1,61 @@
1
+ import { CheckCircle2, CircleAlert, LoaderCircle } from "lucide-react";
2
+ import { cn } from "../../lib/utils";
3
+
4
+ export type MindMapSaveStatus = "saved" | "unsaved" | "saving";
5
+
6
+ interface SaveStatusIndicatorProps {
7
+ status: MindMapSaveStatus;
8
+ className?: string;
9
+ labels?: Partial<Record<MindMapSaveStatus, string>>;
10
+ }
11
+
12
+ const defaultLabels: Record<MindMapSaveStatus, string> = {
13
+ saved: "Salvo",
14
+ unsaved: "Não salvo",
15
+ saving: "Salvando...",
16
+ };
17
+
18
+ export function SaveStatusIndicator({
19
+ status,
20
+ className,
21
+ labels,
22
+ }: SaveStatusIndicatorProps) {
23
+ const mergedLabels = {
24
+ ...defaultLabels,
25
+ ...labels,
26
+ };
27
+
28
+ if (status === "saving") {
29
+ return (
30
+ <span
31
+ aria-label={mergedLabels.saving}
32
+ title={mergedLabels.saving}
33
+ className={cn("inline-flex items-center justify-center", className)}
34
+ >
35
+ <LoaderCircle className="size-4 animate-spin text-sky-600" />
36
+ </span>
37
+ );
38
+ }
39
+
40
+ if (status === "unsaved") {
41
+ return (
42
+ <span
43
+ aria-label={mergedLabels.unsaved}
44
+ title={mergedLabels.unsaved}
45
+ className={cn("inline-flex items-center justify-center", className)}
46
+ >
47
+ <CircleAlert className="size-4 text-amber-600" />
48
+ </span>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <span
54
+ aria-label={mergedLabels.saved}
55
+ title={mergedLabels.saved}
56
+ className={cn("inline-flex items-center justify-center", className)}
57
+ >
58
+ <CheckCircle2 className="size-4 text-emerald-600" />
59
+ </span>
60
+ );
61
+ }
@@ -0,0 +1,270 @@
1
+ import { useMemo } from "react";
2
+ import type { MindMapNode } from "../../state/mindMap";
3
+ import { cn } from "../../lib/utils";
4
+
5
+ interface SegmentsProps {
6
+ nodes: MindMapNode[];
7
+ className?: string;
8
+ }
9
+
10
+ type Point = {
11
+ x: number;
12
+ y: number;
13
+ };
14
+
15
+ export type SegmentLine = {
16
+ key: string;
17
+ start: Point;
18
+ end: Point;
19
+ controlStart: Point;
20
+ controlEnd: Point;
21
+ color: string;
22
+ };
23
+
24
+ const TOGGLE_EDGE_OUTSIDE_OFFSET = 6;
25
+
26
+ export function getNodeWrapper(node: MindMapNode) {
27
+ const wrapperPadding = node.style.wrapperPadding;
28
+
29
+ return {
30
+ left: node.pos.x,
31
+ top: node.pos.y,
32
+ width: node.style.w + wrapperPadding * 2,
33
+ height: node.style.h + wrapperPadding * 2,
34
+ };
35
+ }
36
+
37
+ function getNodeCenter(node: MindMapNode): Point {
38
+ const wrapper = getNodeWrapper(node);
39
+
40
+ return {
41
+ x: wrapper.left + wrapper.width / 2,
42
+ y: wrapper.top + wrapper.height / 2,
43
+ };
44
+ }
45
+
46
+ function getNodeAnchor(node: MindMapNode, side: "left" | "right"): Point {
47
+ const wrapper = getNodeWrapper(node);
48
+
49
+ return {
50
+ x: side === "right" ? wrapper.left + wrapper.width : wrapper.left,
51
+ y: wrapper.top + wrapper.height / 2,
52
+ };
53
+ }
54
+
55
+ function findCentralNode(nodes: MindMapNode[]): MindMapNode | null {
56
+ let centralNode: MindMapNode | null = null;
57
+
58
+ const walk = (node: MindMapNode) => {
59
+ if (centralNode) {
60
+ return;
61
+ }
62
+
63
+ if (node.type === "central") {
64
+ centralNode = node;
65
+ return;
66
+ }
67
+
68
+ for (const child of node.childrens) {
69
+ walk(child);
70
+ }
71
+ };
72
+
73
+ for (const node of nodes) {
74
+ walk(node);
75
+ }
76
+
77
+ return centralNode;
78
+ }
79
+
80
+ function getBranchSide(
81
+ node: MindMapNode,
82
+ centralCenterX: number | null,
83
+ ): "left" | "right" | null {
84
+ if (centralCenterX === null || node.type === "central") {
85
+ return null;
86
+ }
87
+
88
+ return getNodeCenter(node).x < centralCenterX ? "left" : "right";
89
+ }
90
+
91
+ function getOutputAnchor(node: MindMapNode, side: "left" | "right"): Point {
92
+ if (node.type === "central") {
93
+ return {
94
+ x:
95
+ node.pos.x +
96
+ node.style.wrapperPadding +
97
+ (side === "right" ? node.style.w : 0),
98
+ y: node.pos.y + node.style.wrapperPadding + node.style.h / 2,
99
+ };
100
+ }
101
+
102
+ const wrapper = getNodeWrapper(node);
103
+ const hasChildren = node.childrens.length > 0;
104
+
105
+ if (!hasChildren) {
106
+ return getNodeAnchor(node, side);
107
+ }
108
+
109
+ return {
110
+ x:
111
+ side === "right"
112
+ ? wrapper.left + wrapper.width + TOGGLE_EDGE_OUTSIDE_OFFSET
113
+ : wrapper.left - TOGGLE_EDGE_OUTSIDE_OFFSET,
114
+ y: wrapper.top + wrapper.height / 2,
115
+ };
116
+ }
117
+
118
+ function getInputAnchor(node: MindMapNode, side: "left" | "right"): Point {
119
+ if (node.type === "central") {
120
+ return {
121
+ x:
122
+ node.pos.x +
123
+ node.style.wrapperPadding +
124
+ (side === "right" ? 0 : node.style.w),
125
+ y: node.pos.y + node.style.wrapperPadding + node.style.h / 2,
126
+ };
127
+ }
128
+
129
+ const wrapper = getNodeWrapper(node);
130
+
131
+ return {
132
+ x: side === "right" ? wrapper.left + 1 : wrapper.left + wrapper.width - 1,
133
+ y: wrapper.top + wrapper.height / 2 + 0.5,
134
+ };
135
+ }
136
+
137
+ function buildSegmentLine(
138
+ fromNode: MindMapNode,
139
+ toNode: MindMapNode,
140
+ centralCenterX: number | null,
141
+ ): SegmentLine | null {
142
+ const fromCenter = getNodeCenter(fromNode);
143
+ const toCenter = getNodeCenter(toNode);
144
+ const branchSide =
145
+ getBranchSide(toNode, centralCenterX) ??
146
+ (toCenter.x >= fromCenter.x ? "right" : "left");
147
+ const oppositeSide = branchSide === "right" ? "left" : "right";
148
+
149
+ const start = getOutputAnchor(fromNode, branchSide);
150
+ const end = getInputAnchor(toNode, branchSide);
151
+
152
+ const dx = end.x - start.x;
153
+ const dy = end.y - start.y;
154
+ const length = Math.hypot(dx, dy);
155
+
156
+ if (length === 0) {
157
+ return null;
158
+ }
159
+
160
+ const horizontalDistance = Math.abs(dx);
161
+ const verticalDistance = Math.abs(dy);
162
+ const direction = dx >= 0 ? 1 : -1;
163
+ const baseHandle = Math.min(120, Math.max(32, horizontalDistance * 0.45));
164
+ const verticalAdjustment = Math.min(28, verticalDistance * 0.2);
165
+ const handleSize = baseHandle + verticalAdjustment;
166
+
167
+ const controlStart = {
168
+ x: start.x + direction * handleSize,
169
+ y: start.y,
170
+ };
171
+ const controlEnd = {
172
+ x: end.x + (oppositeSide === "right" ? handleSize : -handleSize),
173
+ y: end.y,
174
+ };
175
+
176
+ return {
177
+ key: `${fromNode.id}-${toNode.id}`,
178
+ start,
179
+ end,
180
+ controlStart,
181
+ controlEnd,
182
+ color: toNode.style.color ?? fromNode.style.color ?? "#94a3b8",
183
+ };
184
+ }
185
+
186
+ function collectVisibleEdges(
187
+ parent: MindMapNode,
188
+ edges: Array<{ from: MindMapNode; to: MindMapNode }>,
189
+ ) {
190
+ if (!parent.isVisible) {
191
+ return;
192
+ }
193
+
194
+ for (const child of parent.childrens) {
195
+ if (!child.isVisible) {
196
+ continue;
197
+ }
198
+
199
+ edges.push({ from: parent, to: child });
200
+ collectVisibleEdges(child, edges);
201
+ }
202
+ }
203
+
204
+ export function getSegmentLines(nodes: MindMapNode[]): SegmentLine[] {
205
+ const edges: Array<{ from: MindMapNode; to: MindMapNode }> = [];
206
+ const centralNode = findCentralNode(nodes);
207
+ const centralCenterX = centralNode ? getNodeCenter(centralNode).x : null;
208
+
209
+ for (const node of nodes) {
210
+ collectVisibleEdges(node, edges);
211
+ }
212
+
213
+ return edges
214
+ .map(({ from, to }) => buildSegmentLine(from, to, centralCenterX))
215
+ .filter((segment): segment is SegmentLine => Boolean(segment));
216
+ }
217
+
218
+ export function getNodesBounds(nodes: MindMapNode[]): {
219
+ minX: number;
220
+ minY: number;
221
+ maxX: number;
222
+ maxY: number;
223
+ } | null {
224
+ let minX = Infinity;
225
+ let minY = Infinity;
226
+ let maxX = -Infinity;
227
+ let maxY = -Infinity;
228
+ let hasVisible = false;
229
+
230
+ const walk = (node: MindMapNode) => {
231
+ if (!node.isVisible) return;
232
+ hasVisible = true;
233
+ const w = getNodeWrapper(node);
234
+ minX = Math.min(minX, w.left);
235
+ minY = Math.min(minY, w.top);
236
+ maxX = Math.max(maxX, w.left + w.width);
237
+ maxY = Math.max(maxY, w.top + w.height);
238
+ node.childrens.forEach(walk);
239
+ };
240
+ nodes.forEach(walk);
241
+
242
+ if (!hasVisible) return null;
243
+ return { minX, minY, maxX, maxY };
244
+ }
245
+
246
+ export function Segments({ nodes, className }: SegmentsProps) {
247
+ const segmentLines = useMemo(() => getSegmentLines(nodes), [nodes]);
248
+
249
+ return (
250
+ <svg
251
+ className={cn(
252
+ "absolute left-0 top-0 h-full w-full pointer-events-none overflow-visible",
253
+ className,
254
+ )}
255
+ aria-hidden="true"
256
+ style={{ overflow: "visible" }}
257
+ >
258
+ {segmentLines.map((segment) => (
259
+ <path
260
+ key={segment.key}
261
+ d={`M ${segment.start.x} ${segment.start.y} C ${segment.controlStart.x} ${segment.controlStart.y}, ${segment.controlEnd.x} ${segment.controlEnd.y}, ${segment.end.x} ${segment.end.y}`}
262
+ fill="none"
263
+ stroke={segment.color}
264
+ strokeWidth="6"
265
+ strokeLinecap="round"
266
+ />
267
+ ))}
268
+ </svg>
269
+ );
270
+ }
@@ -0,0 +1,41 @@
1
+ import type { CSSProperties } from "react";
2
+ import { useMindMapState } from "../../state/mindMap";
3
+ import { useShallow } from "zustand/react/shallow";
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export interface ZenCardProps {
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ /** Class for the subtitle line (e.g. "Alt+Z to exit") */
10
+ subtitleClassName?: string;
11
+ }
12
+
13
+ export function ZenCard({
14
+ className,
15
+ style,
16
+ subtitleClassName,
17
+ }: ZenCardProps = {}) {
18
+ const { zenMode } = useMindMapState(
19
+ useShallow((state) => ({ zenMode: state.zenMode })),
20
+ );
21
+ return (
22
+ <div
23
+ data-zen={zenMode}
24
+ className={cn(
25
+ "pointer-events-none absolute bottom-4 right-4 rounded-md border border-slate-200 bg-white/40 px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-500 shadow-sm backdrop-blur transition-all duration-200 data-[zen=true]:opacity-100 data-[zen=true]:translate-y-0 opacity-0 translate-y-2",
26
+ className,
27
+ )}
28
+ style={style}
29
+ >
30
+ <div>Zen mode</div>
31
+ <div
32
+ className={cn(
33
+ "text-[9px] font-medium normal-case tracking-normal text-slate-400",
34
+ subtitleClassName,
35
+ )}
36
+ >
37
+ Alt+Z to exit
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,141 @@
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { XIcon } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ function Dialog({
8
+ ...props
9
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
10
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
11
+ }
12
+
13
+ function DialogTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
16
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
17
+ }
18
+
19
+ function DialogPortal({
20
+ ...props
21
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
22
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
23
+ }
24
+
25
+ function DialogClose({
26
+ ...props
27
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
28
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
29
+ }
30
+
31
+ function DialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
35
+ return (
36
+ <DialogPrimitive.Overlay
37
+ data-slot="dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function DialogContent({
48
+ className,
49
+ children,
50
+ showCloseButton = true,
51
+ ...props
52
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
53
+ showCloseButton?: boolean;
54
+ }) {
55
+ return (
56
+ <DialogPortal data-slot="dialog-portal">
57
+ <DialogOverlay />
58
+ <DialogPrimitive.Content
59
+ data-slot="dialog-content"
60
+ className={cn(
61
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
62
+ className,
63
+ )}
64
+ {...props}
65
+ >
66
+ {children}
67
+ {showCloseButton && (
68
+ <DialogPrimitive.Close
69
+ data-slot="dialog-close"
70
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
71
+ >
72
+ <XIcon />
73
+ <span className="sr-only">Close</span>
74
+ </DialogPrimitive.Close>
75
+ )}
76
+ </DialogPrimitive.Content>
77
+ </DialogPortal>
78
+ );
79
+ }
80
+
81
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="dialog-header"
85
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+
91
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
92
+ return (
93
+ <div
94
+ data-slot="dialog-footer"
95
+ className={cn(
96
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ function DialogTitle({
105
+ className,
106
+ ...props
107
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
108
+ return (
109
+ <DialogPrimitive.Title
110
+ data-slot="dialog-title"
111
+ className={cn("text-lg leading-none font-semibold", className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function DialogDescription({
118
+ className,
119
+ ...props
120
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
121
+ return (
122
+ <DialogPrimitive.Description
123
+ data-slot="dialog-description"
124
+ className={cn("text-muted-foreground text-sm", className)}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ export {
131
+ Dialog,
132
+ DialogClose,
133
+ DialogContent,
134
+ DialogDescription,
135
+ DialogFooter,
136
+ DialogHeader,
137
+ DialogOverlay,
138
+ DialogPortal,
139
+ DialogTitle,
140
+ DialogTrigger,
141
+ };
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ function Popover({
7
+ ...props
8
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
9
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
10
+ }
11
+
12
+ function PopoverTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
15
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
16
+ }
17
+
18
+ function PopoverContent({
19
+ className,
20
+ align = "center",
21
+ sideOffset = 4,
22
+ ...props
23
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
24
+ return (
25
+ <PopoverPrimitive.Portal>
26
+ <PopoverPrimitive.Content
27
+ data-slot="popover-content"
28
+ align={align}
29
+ sideOffset={sideOffset}
30
+ className={cn(
31
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ </PopoverPrimitive.Portal>
37
+ )
38
+ }
39
+
40
+ function PopoverAnchor({
41
+ ...props
42
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
43
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
44
+ }
45
+
46
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }