@zentauri-ui/zentauri-components 1.7.0 → 1.7.2

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 (136) hide show
  1. package/README.md +11 -5
  2. package/cli/index.mjs +1 -0
  3. package/cli/registry.json +3 -0
  4. package/dist/chunk-6QQUQLPB.js +107 -0
  5. package/dist/chunk-6QQUQLPB.js.map +1 -0
  6. package/dist/chunk-BC6M42HQ.mjs +251 -0
  7. package/dist/chunk-BC6M42HQ.mjs.map +1 -0
  8. package/dist/chunk-K6IZANTI.mjs +80 -0
  9. package/dist/chunk-K6IZANTI.mjs.map +1 -0
  10. package/dist/chunk-MTTXLC2V.mjs +100 -0
  11. package/dist/chunk-MTTXLC2V.mjs.map +1 -0
  12. package/dist/chunk-PHEUJ4EF.js +84 -0
  13. package/dist/chunk-PHEUJ4EF.js.map +1 -0
  14. package/dist/chunk-QSPXPU72.js +259 -0
  15. package/dist/chunk-QSPXPU72.js.map +1 -0
  16. package/dist/design-system/checkbox.d.ts +32 -0
  17. package/dist/design-system/checkbox.d.ts.map +1 -0
  18. package/dist/design-system/index.d.ts +3 -0
  19. package/dist/design-system/index.d.ts.map +1 -1
  20. package/dist/design-system/popover.d.ts +40 -0
  21. package/dist/design-system/popover.d.ts.map +1 -0
  22. package/dist/design-system/radio-group.d.ts +37 -0
  23. package/dist/design-system/radio-group.d.ts.map +1 -0
  24. package/dist/ui/checkbox/animated/animations.d.ts +32 -0
  25. package/dist/ui/checkbox/animated/animations.d.ts.map +1 -0
  26. package/dist/ui/checkbox/animated/checkbox-animated.d.ts +6 -0
  27. package/dist/ui/checkbox/animated/checkbox-animated.d.ts.map +1 -0
  28. package/dist/ui/checkbox/animated/index.d.ts +4 -0
  29. package/dist/ui/checkbox/animated/index.d.ts.map +1 -0
  30. package/dist/ui/checkbox/animated/types.d.ts +8 -0
  31. package/dist/ui/checkbox/animated/types.d.ts.map +1 -0
  32. package/dist/ui/checkbox/animated.js +153 -0
  33. package/dist/ui/checkbox/animated.js.map +1 -0
  34. package/dist/ui/checkbox/animated.mjs +150 -0
  35. package/dist/ui/checkbox/animated.mjs.map +1 -0
  36. package/dist/ui/checkbox/checkbox-base.d.ts +6 -0
  37. package/dist/ui/checkbox/checkbox-base.d.ts.map +1 -0
  38. package/dist/ui/checkbox/checkbox.d.ts +6 -0
  39. package/dist/ui/checkbox/checkbox.d.ts.map +1 -0
  40. package/dist/ui/checkbox/index.d.ts +4 -0
  41. package/dist/ui/checkbox/index.d.ts.map +1 -0
  42. package/dist/ui/checkbox/types.d.ts +19 -0
  43. package/dist/ui/checkbox/types.d.ts.map +1 -0
  44. package/dist/ui/checkbox/variants.d.ts +11 -0
  45. package/dist/ui/checkbox/variants.d.ts.map +1 -0
  46. package/dist/ui/checkbox.js +150 -0
  47. package/dist/ui/checkbox.js.map +1 -0
  48. package/dist/ui/checkbox.mjs +137 -0
  49. package/dist/ui/checkbox.mjs.map +1 -0
  50. package/dist/ui/popover/animated/animations.d.ts +3 -0
  51. package/dist/ui/popover/animated/animations.d.ts.map +1 -0
  52. package/dist/ui/popover/animated/index.d.ts +4 -0
  53. package/dist/ui/popover/animated/index.d.ts.map +1 -0
  54. package/dist/ui/popover/animated/popover-content-animated.d.ts +3 -0
  55. package/dist/ui/popover/animated/popover-content-animated.d.ts.map +1 -0
  56. package/dist/ui/popover/animated/types.d.ts +9 -0
  57. package/dist/ui/popover/animated/types.d.ts.map +1 -0
  58. package/dist/ui/popover/animated.js +67 -0
  59. package/dist/ui/popover/animated.js.map +1 -0
  60. package/dist/ui/popover/animated.mjs +64 -0
  61. package/dist/ui/popover/animated.mjs.map +1 -0
  62. package/dist/ui/popover/index.d.ts +4 -0
  63. package/dist/ui/popover/index.d.ts.map +1 -0
  64. package/dist/ui/popover/popover-base.d.ts +8 -0
  65. package/dist/ui/popover/popover-base.d.ts.map +1 -0
  66. package/dist/ui/popover/popover.d.ts +2 -0
  67. package/dist/ui/popover/popover.d.ts.map +1 -0
  68. package/dist/ui/popover/types.d.ts +34 -0
  69. package/dist/ui/popover/types.d.ts.map +1 -0
  70. package/dist/ui/popover/variants.d.ts +6 -0
  71. package/dist/ui/popover/variants.d.ts.map +1 -0
  72. package/dist/ui/popover.js +34 -0
  73. package/dist/ui/popover.js.map +1 -0
  74. package/dist/ui/popover.mjs +5 -0
  75. package/dist/ui/popover.mjs.map +1 -0
  76. package/dist/ui/radio-group/animated/animations.d.ts +32 -0
  77. package/dist/ui/radio-group/animated/animations.d.ts.map +1 -0
  78. package/dist/ui/radio-group/animated/index.d.ts +4 -0
  79. package/dist/ui/radio-group/animated/index.d.ts.map +1 -0
  80. package/dist/ui/radio-group/animated/radio-group-animated.d.ts +10 -0
  81. package/dist/ui/radio-group/animated/radio-group-animated.d.ts.map +1 -0
  82. package/dist/ui/radio-group/animated/types.d.ts +11 -0
  83. package/dist/ui/radio-group/animated/types.d.ts.map +1 -0
  84. package/dist/ui/radio-group/animated.js +177 -0
  85. package/dist/ui/radio-group/animated.js.map +1 -0
  86. package/dist/ui/radio-group/animated.mjs +173 -0
  87. package/dist/ui/radio-group/animated.mjs.map +1 -0
  88. package/dist/ui/radio-group/index.d.ts +4 -0
  89. package/dist/ui/radio-group/index.d.ts.map +1 -0
  90. package/dist/ui/radio-group/radio-group-context.d.ts +13 -0
  91. package/dist/ui/radio-group/radio-group-context.d.ts.map +1 -0
  92. package/dist/ui/radio-group/radio-group.d.ts +10 -0
  93. package/dist/ui/radio-group/radio-group.d.ts.map +1 -0
  94. package/dist/ui/radio-group/types.d.ts +26 -0
  95. package/dist/ui/radio-group/types.d.ts.map +1 -0
  96. package/dist/ui/radio-group/variants.d.ts +14 -0
  97. package/dist/ui/radio-group/variants.d.ts.map +1 -0
  98. package/dist/ui/radio-group.js +171 -0
  99. package/dist/ui/radio-group.js.map +1 -0
  100. package/dist/ui/radio-group.mjs +153 -0
  101. package/dist/ui/radio-group.mjs.map +1 -0
  102. package/package.json +1 -1
  103. package/src/design-system/checkbox.ts +47 -0
  104. package/src/design-system/index.ts +3 -0
  105. package/src/design-system/popover.ts +66 -0
  106. package/src/design-system/radio-group.ts +54 -0
  107. package/src/ui/checkbox/animated/animations.ts +12 -0
  108. package/src/ui/checkbox/animated/checkbox-animated.tsx +145 -0
  109. package/src/ui/checkbox/animated/index.ts +9 -0
  110. package/src/ui/checkbox/animated/types.ts +9 -0
  111. package/src/ui/checkbox/checkbox-base.tsx +134 -0
  112. package/src/ui/checkbox/checkbox.test.tsx +53 -0
  113. package/src/ui/checkbox/checkbox.tsx +8 -0
  114. package/src/ui/checkbox/index.ts +15 -0
  115. package/src/ui/checkbox/types.ts +40 -0
  116. package/src/ui/checkbox/variants.ts +50 -0
  117. package/src/ui/popover/animated/animations.ts +15 -0
  118. package/src/ui/popover/animated/index.ts +10 -0
  119. package/src/ui/popover/animated/popover-content-animated.tsx +54 -0
  120. package/src/ui/popover/animated/types.ts +18 -0
  121. package/src/ui/popover/index.ts +18 -0
  122. package/src/ui/popover/popover-base.tsx +261 -0
  123. package/src/ui/popover/popover.test.tsx +84 -0
  124. package/src/ui/popover/popover.tsx +8 -0
  125. package/src/ui/popover/types.ts +38 -0
  126. package/src/ui/popover/variants.ts +21 -0
  127. package/src/ui/radio-group/animated/animations.ts +12 -0
  128. package/src/ui/radio-group/animated/index.ts +10 -0
  129. package/src/ui/radio-group/animated/radio-group-animated.tsx +173 -0
  130. package/src/ui/radio-group/animated/types.ts +13 -0
  131. package/src/ui/radio-group/index.ts +19 -0
  132. package/src/ui/radio-group/radio-group-context.ts +23 -0
  133. package/src/ui/radio-group/radio-group.test.tsx +61 -0
  134. package/src/ui/radio-group/radio-group.tsx +159 -0
  135. package/src/ui/radio-group/types.ts +62 -0
  136. package/src/ui/radio-group/variants.ts +61 -0
@@ -0,0 +1,261 @@
1
+ "use client";
2
+
3
+ import {
4
+ Children,
5
+ cloneElement,
6
+ createContext,
7
+ isValidElement,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useId,
12
+ useRef,
13
+ useState,
14
+ type RefObject,
15
+ type KeyboardEventHandler,
16
+ type MouseEventHandler,
17
+ type ReactElement,
18
+ type Ref,
19
+ useMemo,
20
+ } from "react";
21
+
22
+ import { cn } from "../../lib/utils";
23
+
24
+ import type {
25
+ PopoverAlign,
26
+ PopoverContentProps,
27
+ PopoverContextType,
28
+ PopoverProps,
29
+ PopoverSide,
30
+ PopoverTriggerProps,
31
+ } from "./types";
32
+ import { popoverContentVariants } from "./variants";
33
+
34
+ export const PopoverContext = createContext<PopoverContextType | null>(null);
35
+
36
+ export const usePopover = () => {
37
+ const context = useContext(PopoverContext);
38
+ if (!context) {
39
+ throw new Error("Popover components must be used within Popover");
40
+ }
41
+ return context;
42
+ };
43
+
44
+ function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {
45
+ return (node: T) => {
46
+ for (const ref of refs) {
47
+ if (typeof ref === "function") {
48
+ ref(node);
49
+ } else if (ref) {
50
+ (ref as RefObject<T | null>).current = node;
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ export function sideAlignClass(side: PopoverSide, align: PopoverAlign) {
57
+ const sideClasses = {
58
+ top: "bottom-full mb-2",
59
+ bottom: "top-full mt-2",
60
+ left: "right-full mr-2",
61
+ right: "left-full ml-2",
62
+ } satisfies Record<PopoverSide, string>;
63
+
64
+ const verticalAlign = {
65
+ start: "left-0",
66
+ center: "left-1/2 -translate-x-1/2",
67
+ end: "right-0",
68
+ } satisfies Record<PopoverAlign, string>;
69
+
70
+ const horizontalAlign = {
71
+ start: "top-0",
72
+ center: "top-1/2 -translate-y-1/2",
73
+ end: "bottom-0",
74
+ } satisfies Record<PopoverAlign, string>;
75
+
76
+ return cn(
77
+ sideClasses[side],
78
+ side === "top" || side === "bottom"
79
+ ? verticalAlign[align]
80
+ : horizontalAlign[align],
81
+ );
82
+ }
83
+
84
+ export const Popover = ({
85
+ children,
86
+ defaultOpen = false,
87
+ open: controlledOpen,
88
+ onOpenChange,
89
+ closeOnEscape = true,
90
+ closeOnOutsideClick = true,
91
+ }: PopoverProps) => {
92
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
93
+ const contentId = `${useId()}-popover`;
94
+ const triggerRef = useRef<HTMLElement | null>(null);
95
+ const contentRef = useRef<HTMLDivElement | null>(null);
96
+
97
+ const isControlled = controlledOpen !== undefined;
98
+ const open = isControlled ? controlledOpen : uncontrolledOpen;
99
+
100
+ const setOpen = useCallback(
101
+ (value: boolean) => {
102
+ if (!isControlled) {
103
+ setUncontrolledOpen(value);
104
+ }
105
+ onOpenChange?.(value);
106
+ },
107
+ [isControlled, onOpenChange],
108
+ );
109
+
110
+ const toggleOpen = useCallback(() => setOpen(!open), [open, setOpen]);
111
+
112
+ useEffect(() => {
113
+ if (!open) {
114
+ return undefined;
115
+ }
116
+
117
+ const onPointerDown = (event: PointerEvent) => {
118
+ if (!closeOnOutsideClick) {
119
+ return;
120
+ }
121
+ const target = event.target as Node;
122
+ if (
123
+ contentRef.current?.contains(target) ||
124
+ triggerRef.current?.contains(target)
125
+ ) {
126
+ return;
127
+ }
128
+ setOpen(false);
129
+ };
130
+
131
+ const onKeyDown = (event: KeyboardEvent) => {
132
+ if (event.key === "Escape" && closeOnEscape) {
133
+ setOpen(false);
134
+ triggerRef.current?.focus();
135
+ }
136
+ };
137
+
138
+ document.addEventListener("pointerdown", onPointerDown);
139
+ document.addEventListener("keydown", onKeyDown);
140
+
141
+ return () => {
142
+ document.removeEventListener("pointerdown", onPointerDown);
143
+ document.removeEventListener("keydown", onKeyDown);
144
+ };
145
+ }, [closeOnEscape, closeOnOutsideClick, open, setOpen]);
146
+
147
+ const contextValue = useMemo(
148
+ () => ({
149
+ open,
150
+ setOpen,
151
+ toggleOpen,
152
+ contentId,
153
+ triggerRef,
154
+ contentRef,
155
+ }),
156
+ [open, setOpen, toggleOpen, contentId],
157
+ );
158
+
159
+ return (
160
+ <PopoverContext.Provider
161
+ value={contextValue}
162
+ >
163
+ <div className="relative inline-block">{children}</div>
164
+ </PopoverContext.Provider>
165
+ );
166
+ };
167
+
168
+ export const PopoverTrigger = ({
169
+ children,
170
+ className,
171
+ }: PopoverTriggerProps) => {
172
+ const { open, toggleOpen, contentId, triggerRef } = usePopover();
173
+ const childList = Children.toArray(children).filter(
174
+ (node) => node !== null && node !== undefined && typeof node !== "boolean",
175
+ );
176
+
177
+ const soleCandidate =
178
+ childList.length === 1 && isValidElement(childList[0])
179
+ ? (childList[0] as ReactElement<{
180
+ className?: string;
181
+ ref?: Ref<HTMLElement>;
182
+ onClick?: MouseEventHandler;
183
+ onKeyDown?: KeyboardEventHandler;
184
+ "aria-expanded"?: boolean;
185
+ "aria-haspopup"?: string;
186
+ "aria-controls"?: string;
187
+ }>)
188
+ : undefined;
189
+
190
+ if (soleCandidate) {
191
+ return cloneElement(soleCandidate, {
192
+ ref: mergeRefs(triggerRef, soleCandidate.props.ref),
193
+ onClick: (event) => {
194
+ soleCandidate.props.onClick?.(event);
195
+ if (!event.defaultPrevented) {
196
+ toggleOpen();
197
+ }
198
+ },
199
+ onKeyDown: (event) => {
200
+ soleCandidate.props.onKeyDown?.(event);
201
+ if (event.key === "Escape") {
202
+ event.preventDefault();
203
+ }
204
+ },
205
+ className: cn(className, soleCandidate.props.className),
206
+ "aria-expanded": open,
207
+ "aria-haspopup": "dialog",
208
+ "aria-controls": open ? contentId : undefined,
209
+ });
210
+ }
211
+
212
+ return (
213
+ <button
214
+ ref={triggerRef as Ref<HTMLButtonElement>}
215
+ type="button"
216
+ className={className}
217
+ aria-expanded={open}
218
+ aria-haspopup="dialog"
219
+ aria-controls={open ? contentId : undefined}
220
+ onClick={toggleOpen}
221
+ >
222
+ {children}
223
+ </button>
224
+ );
225
+ };
226
+
227
+ export const PopoverContent = ({
228
+ children,
229
+ className,
230
+ variant,
231
+ size,
232
+ width,
233
+ side = "bottom",
234
+ align = "center",
235
+ role = "dialog",
236
+ ...props
237
+ }: PopoverContentProps) => {
238
+ const { open, contentId, contentRef } = usePopover();
239
+
240
+ if (!open) {
241
+ return null;
242
+ }
243
+
244
+ return (
245
+ <div
246
+ ref={contentRef}
247
+ id={contentId}
248
+ data-open={open}
249
+ role={role}
250
+ tabIndex={-1}
251
+ className={cn(
252
+ popoverContentVariants({ variant, size, width }),
253
+ sideAlignClass(side, align),
254
+ className,
255
+ )}
256
+ {...props}
257
+ >
258
+ {children}
259
+ </div>
260
+ );
261
+ };
@@ -0,0 +1,84 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
6
+
7
+ describe("Popover", () => {
8
+ it("should open and close popover content from the trigger", async () => {
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Popover>
12
+ <PopoverTrigger>
13
+ <button type="button">Open panel</button>
14
+ </PopoverTrigger>
15
+ <PopoverContent>Interactive panel</PopoverContent>
16
+ </Popover>,
17
+ );
18
+
19
+ const trigger = screen.getByRole("button", { name: "Open panel" });
20
+ await user.click(trigger);
21
+ expect(screen.getByRole("dialog")).toHaveTextContent("Interactive panel");
22
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
23
+
24
+ await user.click(trigger);
25
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
26
+ });
27
+
28
+ it("should close when clicking outside the popover", async () => {
29
+ const user = userEvent.setup();
30
+ render(
31
+ <>
32
+ <Popover defaultOpen>
33
+ <PopoverTrigger>
34
+ <button type="button">Open panel</button>
35
+ </PopoverTrigger>
36
+ <PopoverContent>Panel body</PopoverContent>
37
+ </Popover>
38
+ <button type="button">Outside</button>
39
+ </>,
40
+ );
41
+
42
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
43
+ await user.click(screen.getByRole("button", { name: "Outside" }));
44
+ await waitFor(() => {
45
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
46
+ });
47
+ });
48
+
49
+ it("should close on Escape and return focus to the trigger", async () => {
50
+ const user = userEvent.setup();
51
+ render(
52
+ <Popover defaultOpen>
53
+ <PopoverTrigger>
54
+ <button type="button">Focus trigger</button>
55
+ </PopoverTrigger>
56
+ <PopoverContent>Escape closes me</PopoverContent>
57
+ </Popover>,
58
+ );
59
+
60
+ await user.keyboard("{Escape}");
61
+ await waitFor(() => {
62
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
63
+ });
64
+ expect(screen.getByRole("button", { name: "Focus trigger" })).toHaveFocus();
65
+ });
66
+
67
+ it("should support controlled open state", async () => {
68
+ const user = userEvent.setup();
69
+ const onOpenChange = vi.fn();
70
+
71
+ render(
72
+ <Popover open={false} onOpenChange={onOpenChange}>
73
+ <PopoverTrigger>
74
+ <button type="button">Controlled</button>
75
+ </PopoverTrigger>
76
+ <PopoverContent>Controlled body</PopoverContent>
77
+ </Popover>,
78
+ );
79
+
80
+ await user.click(screen.getByRole("button", { name: "Controlled" }));
81
+ expect(onOpenChange).toHaveBeenCalledWith(true);
82
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
83
+ });
84
+ });
@@ -0,0 +1,8 @@
1
+ // popover.tsx — default static entry (no framer-motion)
2
+ export {
3
+ Popover,
4
+ PopoverTrigger,
5
+ PopoverContent,
6
+ PopoverContext,
7
+ usePopover,
8
+ } from "./popover-base";
@@ -0,0 +1,38 @@
1
+ import { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+ import { popoverContentVariants } from "./variants";
4
+
5
+ export type PopoverSide = "top" | "left" | "bottom" | "right";
6
+ export type PopoverAlign = "start" | "center" | "end";
7
+
8
+ export type PopoverContextType = {
9
+ open: boolean;
10
+ setOpen: (value: boolean) => void;
11
+ toggleOpen: () => void;
12
+ contentId: string;
13
+ triggerRef: React.RefObject<HTMLElement | null>;
14
+ contentRef: React.RefObject<HTMLDivElement | null>;
15
+ };
16
+
17
+ export type PopoverProps = {
18
+ children: ReactNode;
19
+ defaultOpen?: boolean;
20
+ open?: boolean;
21
+ onOpenChange?: (open: boolean) => void;
22
+ closeOnEscape?: boolean;
23
+ closeOnOutsideClick?: boolean;
24
+ };
25
+
26
+ export type PopoverTriggerProps = {
27
+ children: ReactNode;
28
+ className?: string;
29
+ };
30
+
31
+ export type PopoverContentProps = ComponentPropsWithRef<"div"> & {
32
+ children: ReactNode;
33
+ variant?: VariantProps<typeof popoverContentVariants>["variant"]
34
+ size?: VariantProps<typeof popoverContentVariants>["size"];
35
+ width?: VariantProps<typeof popoverContentVariants>["width"];
36
+ side?: PopoverSide;
37
+ align?: PopoverAlign;
38
+ };
@@ -0,0 +1,21 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiPopoverContentBase,
5
+ zuiPopoverContentSizes,
6
+ zuiPopoverContentVariants,
7
+ zuiPopoverContentWidths,
8
+ } from "../../design-system/popover";
9
+
10
+ export const popoverContentVariants = cva(zuiPopoverContentBase, {
11
+ variants: {
12
+ variant: zuiPopoverContentVariants,
13
+ size: zuiPopoverContentSizes,
14
+ width: zuiPopoverContentWidths,
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ size: "md",
19
+ width: "xs",
20
+ },
21
+ });
@@ -0,0 +1,12 @@
1
+ export const radioGroupAnimationPresets = {
2
+ pop: {
3
+ initial: { scale: 0.35, opacity: 0 },
4
+ animate: { scale: 1, opacity: 1 },
5
+ transition: { type: "spring", stiffness: 520, damping: 28 },
6
+ },
7
+ fade: {
8
+ initial: { scale: 0.85, opacity: 0 },
9
+ animate: { scale: 1, opacity: 1 },
10
+ transition: { duration: 0.16, ease: "easeOut" },
11
+ },
12
+ } as const;
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export { RadioGroupAnimated, RadioGroupItemAnimated } from "./radio-group-animated";
4
+ export type {
5
+ RadioGroupAnimatedProps,
6
+ RadioGroupAnimation,
7
+ RadioGroupAnimationPresets,
8
+ RadioGroupItemAnimatedProps,
9
+ } from "./types";
10
+ export { radioGroupAnimationPresets } from "./animations";
@@ -0,0 +1,173 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import { useCallback, useId, useState } from "react";
5
+
6
+ import { cn } from "../../../lib/utils";
7
+
8
+ import { radioGroupAnimationPresets } from "./animations";
9
+ import type {
10
+ RadioGroupAnimatedProps,
11
+ RadioGroupItemAnimatedProps,
12
+ } from "./types";
13
+ import {
14
+ RadioGroupContext,
15
+ useRadioGroupContext,
16
+ } from "../radio-group-context";
17
+ import {
18
+ radioGroupControlVariants,
19
+ radioGroupIndicatorVariants,
20
+ radioGroupItemVariants,
21
+ radioGroupRootVariants,
22
+ } from "../variants";
23
+
24
+ export function RadioGroupAnimated(props: RadioGroupAnimatedProps) {
25
+ const {
26
+ className,
27
+ value,
28
+ defaultValue,
29
+ name,
30
+ disabled,
31
+ required,
32
+ onValueChange,
33
+ orientation,
34
+ appearance,
35
+ size,
36
+ children,
37
+ ref,
38
+ animation: _animation,
39
+ ...rest
40
+ } = props;
41
+ const generatedName = useId();
42
+ const isControlled = value !== undefined;
43
+ const [uncontrolled, setUncontrolled] = useState(defaultValue);
44
+ const resolvedValue = isControlled ? value : uncontrolled;
45
+
46
+ const setValue = useCallback(
47
+ (next: string) => {
48
+ if (!isControlled) {
49
+ setUncontrolled(next);
50
+ }
51
+ onValueChange?.(next);
52
+ },
53
+ [isControlled, onValueChange],
54
+ );
55
+
56
+ return (
57
+ <RadioGroupContext.Provider
58
+ value={{
59
+ value: resolvedValue,
60
+ name: name ?? generatedName,
61
+ disabled,
62
+ required,
63
+ appearance: appearance ?? undefined,
64
+ size: size ?? undefined,
65
+ onValueChange: setValue,
66
+ }}
67
+ >
68
+ <div
69
+ ref={ref}
70
+ role="radiogroup"
71
+ data-slot="radio-group"
72
+ data-orientation={orientation ?? "vertical"}
73
+ className={cn(radioGroupRootVariants({ orientation }), className)}
74
+ {...rest}
75
+ >
76
+ {children}
77
+ </div>
78
+ </RadioGroupContext.Provider>
79
+ );
80
+ }
81
+
82
+ RadioGroupAnimated.displayName = "RadioGroup";
83
+
84
+ export function RadioGroupItemAnimated(props: RadioGroupItemAnimatedProps) {
85
+ const {
86
+ className,
87
+ rootClassName,
88
+ controlClassName,
89
+ indicatorClassName,
90
+ value,
91
+ appearance: appearanceProp,
92
+ size: sizeProp,
93
+ disabled: disabledProp,
94
+ required: requiredProp,
95
+ children,
96
+ label,
97
+ id,
98
+ ref,
99
+ "aria-label": ariaLabel,
100
+ animation = "pop",
101
+ ...rest
102
+ } = props;
103
+ const generatedId = useId();
104
+ const context = useRadioGroupContext();
105
+ const controlId = id ?? generatedId;
106
+ const checked = context?.value === value;
107
+ const disabled = disabledProp ?? context?.disabled;
108
+ const required = requiredProp ?? context?.required;
109
+ const appearance = appearanceProp ?? context?.appearance;
110
+ const size = sizeProp ?? context?.size;
111
+ const labelContent = label ?? children;
112
+ const hasVisibleLabel =
113
+ labelContent !== undefined && labelContent !== null && labelContent !== false;
114
+ const motionPreset = radioGroupAnimationPresets[animation];
115
+
116
+ return (
117
+ <label
118
+ className={cn(radioGroupItemVariants({ size }), rootClassName, className)}
119
+ data-disabled={disabled ? "true" : undefined}
120
+ data-state={checked ? "checked" : "unchecked"}
121
+ htmlFor={controlId}
122
+ >
123
+ <input
124
+ ref={ref}
125
+ id={controlId}
126
+ type="radio"
127
+ data-slot="radio-group-item"
128
+ className="peer sr-only"
129
+ name={context?.name}
130
+ value={value}
131
+ checked={checked}
132
+ disabled={disabled}
133
+ required={required}
134
+ aria-label={ariaLabel ?? (hasVisibleLabel ? undefined : value)}
135
+ onChange={(event) => {
136
+ if (event.currentTarget.checked) {
137
+ context?.onValueChange(value);
138
+ }
139
+ }}
140
+ {...rest}
141
+ />
142
+ <span
143
+ aria-hidden="true"
144
+ className={cn(
145
+ radioGroupControlVariants({ appearance, size }),
146
+ controlClassName,
147
+ )}
148
+ data-slot="radio-group-control"
149
+ >
150
+ {checked && (
151
+ <motion.span
152
+ className={cn(
153
+ radioGroupIndicatorVariants({ size }),
154
+ "opacity-100",
155
+ indicatorClassName,
156
+ )}
157
+ data-slot="radio-group-indicator"
158
+ initial={motionPreset.initial}
159
+ animate={motionPreset.animate}
160
+ transition={motionPreset.transition}
161
+ />
162
+ )}
163
+ </span>
164
+ {hasVisibleLabel && (
165
+ <span className="min-w-0 leading-6" data-slot="radio-group-label">
166
+ {labelContent}
167
+ </span>
168
+ )}
169
+ </label>
170
+ );
171
+ }
172
+
173
+ RadioGroupItemAnimated.displayName = "RadioGroupItem";
@@ -0,0 +1,13 @@
1
+ import type { RadioGroupItemProps, RadioGroupProps } from "../types";
2
+ import type { radioGroupAnimationPresets } from "./animations";
3
+
4
+ export type RadioGroupAnimation = keyof typeof radioGroupAnimationPresets;
5
+ export type RadioGroupAnimationPresets = typeof radioGroupAnimationPresets;
6
+
7
+ export type RadioGroupAnimatedProps = RadioGroupProps & {
8
+ animation?: RadioGroupAnimation;
9
+ };
10
+
11
+ export type RadioGroupItemAnimatedProps = RadioGroupItemProps & {
12
+ animation?: RadioGroupAnimation;
13
+ };
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ export { RadioGroup, RadioGroupItem } from "./radio-group";
4
+ export type {
5
+ RadioGroupAppearance,
6
+ RadioGroupControlVariantProps,
7
+ RadioGroupIndicatorVariantProps,
8
+ RadioGroupItemProps,
9
+ RadioGroupItemVariantProps,
10
+ RadioGroupProps,
11
+ RadioGroupRootVariantProps,
12
+ RadioGroupSize,
13
+ } from "./types";
14
+ export {
15
+ radioGroupControlVariants,
16
+ radioGroupIndicatorVariants,
17
+ radioGroupItemVariants,
18
+ radioGroupRootVariants,
19
+ } from "./variants";
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+
5
+ import type { RadioGroupAppearance, RadioGroupSize } from "./types";
6
+
7
+ export type RadioGroupContextValue = {
8
+ value: string | undefined;
9
+ name: string;
10
+ disabled?: boolean;
11
+ required?: boolean;
12
+ appearance?: RadioGroupAppearance;
13
+ size?: RadioGroupSize;
14
+ onValueChange: (value: string) => void;
15
+ };
16
+
17
+ export const RadioGroupContext = createContext<RadioGroupContextValue | null>(
18
+ null,
19
+ );
20
+
21
+ export function useRadioGroupContext() {
22
+ return useContext(RadioGroupContext);
23
+ }