@cntyclub/ui-react 0.1.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.
Files changed (124) hide show
  1. package/dist/chunk-HDGMSYQS.js +26461 -0
  2. package/dist/chunk-HDGMSYQS.js.map +1 -0
  3. package/dist/chunk-PR4QN5HX.js +39 -0
  4. package/dist/chunk-PR4QN5HX.js.map +1 -0
  5. package/dist/form.d.ts +175 -0
  6. package/dist/form.js +5207 -0
  7. package/dist/form.js.map +1 -0
  8. package/dist/index.d.ts +1462 -0
  9. package/dist/index.js +81862 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/input-CZvh825j.d.ts +24 -0
  12. package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
  13. package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
  14. package/package.json +79 -0
  15. package/src/components/form/checkbox-group-field.tsx +101 -0
  16. package/src/components/form/date-field.tsx +79 -0
  17. package/src/components/form/date-range-field.tsx +106 -0
  18. package/src/components/form/form-context.ts +10 -0
  19. package/src/components/form/form.tsx +54 -0
  20. package/src/components/form/number-field.tsx +69 -0
  21. package/src/components/form/select-field.tsx +76 -0
  22. package/src/components/form/submit-button.tsx +28 -0
  23. package/src/components/form/text-field.tsx +107 -0
  24. package/src/components/layout/dashboard-header.tsx +54 -0
  25. package/src/components/layout/dashboard-panel.tsx +34 -0
  26. package/src/components/theme-provider.tsx +403 -0
  27. package/src/components/ui/accordion.tsx +69 -0
  28. package/src/components/ui/alert-dialog.tsx +169 -0
  29. package/src/components/ui/alert.tsx +80 -0
  30. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  31. package/src/components/ui/app-store-buttons.tsx +182 -0
  32. package/src/components/ui/aspect-ratio.tsx +23 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +95 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/checkbox-group.tsx +16 -0
  45. package/src/components/ui/checkbox.tsx +82 -0
  46. package/src/components/ui/collapsible.tsx +45 -0
  47. package/src/components/ui/combobox.tsx +411 -0
  48. package/src/components/ui/command.tsx +264 -0
  49. package/src/components/ui/context-menu.tsx +271 -0
  50. package/src/components/ui/credit-card.tsx +214 -0
  51. package/src/components/ui/dialog.tsx +196 -0
  52. package/src/components/ui/drawer.tsx +135 -0
  53. package/src/components/ui/empty.tsx +127 -0
  54. package/src/components/ui/featured-icon.tsx +149 -0
  55. package/src/components/ui/field.tsx +88 -0
  56. package/src/components/ui/fieldset.tsx +29 -0
  57. package/src/components/ui/form.tsx +17 -0
  58. package/src/components/ui/frame.tsx +82 -0
  59. package/src/components/ui/generic-empty.tsx +142 -0
  60. package/src/components/ui/group.tsx +97 -0
  61. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  62. package/src/components/ui/input-group.tsx +102 -0
  63. package/src/components/ui/input-otp.tsx +96 -0
  64. package/src/components/ui/input.tsx +66 -0
  65. package/src/components/ui/item.tsx +198 -0
  66. package/src/components/ui/kbd.tsx +30 -0
  67. package/src/components/ui/label.tsx +28 -0
  68. package/src/components/ui/menu.tsx +312 -0
  69. package/src/components/ui/menubar.tsx +93 -0
  70. package/src/components/ui/meter.tsx +67 -0
  71. package/src/components/ui/multi-select.tsx +308 -0
  72. package/src/components/ui/navigation-menu.tsx +143 -0
  73. package/src/components/ui/number-field.tsx +160 -0
  74. package/src/components/ui/pagination-controls.tsx +74 -0
  75. package/src/components/ui/pagination.tsx +149 -0
  76. package/src/components/ui/popover.tsx +119 -0
  77. package/src/components/ui/preview-card.tsx +55 -0
  78. package/src/components/ui/progress.tsx +289 -0
  79. package/src/components/ui/qr-code.tsx +150 -0
  80. package/src/components/ui/radio-group.tsx +103 -0
  81. package/src/components/ui/resizable.tsx +56 -0
  82. package/src/components/ui/scroll-area.tsx +90 -0
  83. package/src/components/ui/scroller.tsx +38 -0
  84. package/src/components/ui/section-header.tsx +118 -0
  85. package/src/components/ui/select.tsx +181 -0
  86. package/src/components/ui/separator.tsx +23 -0
  87. package/src/components/ui/sheet.tsx +224 -0
  88. package/src/components/ui/sidebar.tsx +744 -0
  89. package/src/components/ui/skeleton.tsx +16 -0
  90. package/src/components/ui/slider.tsx +108 -0
  91. package/src/components/ui/smooth-scroll.tsx +143 -0
  92. package/src/components/ui/social-button.tsx +247 -0
  93. package/src/components/ui/spinner-on-demand.tsx +32 -0
  94. package/src/components/ui/spinner.tsx +18 -0
  95. package/src/components/ui/stat.tsx +187 -0
  96. package/src/components/ui/stepper.tsx +167 -0
  97. package/src/components/ui/switch.tsx +56 -0
  98. package/src/components/ui/table.tsx +126 -0
  99. package/src/components/ui/tabs.tsx +90 -0
  100. package/src/components/ui/tag.tsx +229 -0
  101. package/src/components/ui/target-countdown.tsx +46 -0
  102. package/src/components/ui/text-editor.tsx +313 -0
  103. package/src/components/ui/textarea.tsx +51 -0
  104. package/src/components/ui/timeline.tsx +116 -0
  105. package/src/components/ui/toast.tsx +268 -0
  106. package/src/components/ui/toggle-group.tsx +101 -0
  107. package/src/components/ui/toggle.tsx +45 -0
  108. package/src/components/ui/toolbar.tsx +89 -0
  109. package/src/components/ui/tooltip.tsx +102 -0
  110. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  111. package/src/components/ui/video-player.tsx +275 -0
  112. package/src/components/upload/avatar-upload-base.tsx +131 -0
  113. package/src/components/upload/image-upload-base.tsx +112 -0
  114. package/src/form.ts +17 -0
  115. package/src/index.ts +125 -0
  116. package/src/lib/hooks/use-callback-ref.ts +15 -0
  117. package/src/lib/hooks/use-first-render.ts +11 -0
  118. package/src/lib/hooks/use-hover.ts +53 -0
  119. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  120. package/src/lib/hooks/use-media-query.ts +164 -0
  121. package/src/lib/utils/css.ts +6 -0
  122. package/src/styles.css +300 -0
  123. package/src/types/helpers.ts +24 -0
  124. package/src/types/react.d.ts +7 -0
@@ -0,0 +1,142 @@
1
+ import {
2
+ FileQuestionIcon,
3
+ ShieldAlertIcon,
4
+ ShieldXIcon,
5
+ TriangleAlertIcon,
6
+ } from "lucide-react";
7
+ import type React from "react";
8
+ import {
9
+ Empty,
10
+ EmptyContent,
11
+ EmptyDescription,
12
+ EmptyHeader,
13
+ EmptyMedia,
14
+ EmptyTitle,
15
+ } from "./empty";
16
+
17
+ interface ErrorOrEmptyStateProps
18
+ extends Omit<React.ComponentProps<"div">, "title"> {
19
+ title?: React.ReactNode;
20
+ description?: React.ReactNode;
21
+ actions?: React.ReactNode;
22
+ media?: React.ReactNode;
23
+ mediaVariant?: "icon" | "default";
24
+ compact?: boolean;
25
+ }
26
+
27
+ function ErrorOrEmptyState({
28
+ title,
29
+ description,
30
+ actions,
31
+ className,
32
+ media,
33
+ mediaVariant = "default",
34
+ compact,
35
+ ...props
36
+ }: ErrorOrEmptyStateProps) {
37
+ return (
38
+ <Empty {...props}>
39
+ <EmptyHeader>
40
+ {!compact && <EmptyMedia variant={mediaVariant}>{media}</EmptyMedia>}
41
+ <EmptyTitle>{title}</EmptyTitle>
42
+ <EmptyDescription>{description}</EmptyDescription>
43
+ </EmptyHeader>
44
+ <EmptyContent>{actions}</EmptyContent>
45
+ </Empty>
46
+ );
47
+ }
48
+
49
+ type ErrorTemplateProps = Omit<
50
+ ErrorOrEmptyStateProps,
51
+ "image" | "title" | "description"
52
+ > & {
53
+ mediaClassName?: string;
54
+ };
55
+
56
+ function NotFoundErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
57
+ return (
58
+ <ErrorOrEmptyState
59
+ media={<FileQuestionIcon className={mediaClassName} />}
60
+ mediaVariant="icon"
61
+ title="Not found"
62
+ description="The requested resource was not found."
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function UnauthorizedErrorState({
69
+ mediaClassName,
70
+ ...props
71
+ }: ErrorTemplateProps) {
72
+ return (
73
+ <ErrorOrEmptyState
74
+ media={<ShieldAlertIcon className={mediaClassName} />}
75
+ mediaVariant="icon"
76
+ title="Unauthorized"
77
+ description="You are not authorized to access this resource."
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function ForbiddenErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
84
+ return (
85
+ <ErrorOrEmptyState
86
+ media={<ShieldXIcon className={mediaClassName} />}
87
+ mediaVariant="icon"
88
+ title="Forbidden"
89
+ description="You are not allowed to access this resource."
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function InternalServerErrorState({
96
+ mediaClassName,
97
+ ...props
98
+ }: ErrorTemplateProps) {
99
+ return (
100
+ <ErrorOrEmptyState
101
+ media={<ShieldXIcon className={mediaClassName} />}
102
+ mediaVariant="icon"
103
+ title="Internal server error"
104
+ description="An error occurred while processing your request."
105
+ {...props}
106
+ />
107
+ );
108
+ }
109
+
110
+ function OfflineErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
111
+ return (
112
+ <ErrorOrEmptyState
113
+ media={<ShieldXIcon className={mediaClassName} />}
114
+ mediaVariant="icon"
115
+ title="Offline"
116
+ description="Please check your internet connection and try again."
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ function GeneralErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
123
+ return (
124
+ <ErrorOrEmptyState
125
+ media={<TriangleAlertIcon className={mediaClassName} />}
126
+ mediaVariant="icon"
127
+ title="Error"
128
+ description="An error occurred while processing your request."
129
+ {...props}
130
+ />
131
+ );
132
+ }
133
+
134
+ export {
135
+ ErrorOrEmptyState,
136
+ NotFoundErrorState,
137
+ UnauthorizedErrorState,
138
+ ForbiddenErrorState,
139
+ InternalServerErrorState,
140
+ OfflineErrorState,
141
+ GeneralErrorState,
142
+ };
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { mergeProps } from "@base-ui/react/merge-props";
4
+ import { useRender } from "@base-ui/react/use-render";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import type * as React from "react";
7
+ import { Separator } from "./separator";
8
+ import { cn } from "../../lib/utils/css";
9
+
10
+ const groupVariants = cva(
11
+ "flex w-fit *:focus-visible:z-1 has-[>[data-slot=group]]:gap-2 *:has-focus-visible:z-1 dark:*:[[data-slot=button]:hover~[data-slot=separator]:not([data-slot]:hover~[data-slot=separator]~[data-slot=separator]),[data-slot][data-pressed]~[data-slot=separator]:not([data-slot][data-pressed]~[data-slot=separator]~[data-slot=separator])]:before:bg-input/64 dark:*:[[data-slot=separator]:has(~[data-slot=button]:hover):not(:has(~[data-slot=separator]~[data-slot]:hover)),[data-slot=separator]:has(~[data-slot][data-pressed]):not(:has(~[data-slot=separator]~[data-slot][data-pressed]))]:before:bg-input/64",
12
+ {
13
+ defaultVariants: {
14
+ orientation: "horizontal",
15
+ },
16
+ variants: {
17
+ orientation: {
18
+ horizontal:
19
+ "*:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-start-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-end-[0.5px] *:pointer-coarse:after:min-w-auto *:data-slot:has-[~[data-slot]]:rounded-e-none *:data-slot:has-[~[data-slot]]:border-e-0 *:data-slot:has-[~[data-slot]]:before:rounded-e-none *:[[data-slot]~[data-slot]]:rounded-s-none *:[[data-slot]~[data-slot]]:border-s-0 *:[[data-slot]~[data-slot]]:before:rounded-s-none",
20
+ vertical:
21
+ "*:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-top-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-bottom-[0.5px] flex-col *:pointer-coarse:after:min-h-auto *:data-slot:has-[~[data-slot]]:rounded-b-none *:data-slot:has-[~[data-slot]]:border-b-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:hidden *:data-slot:has-[~[data-slot]]:before:rounded-b-none dark:*:last:before:hidden dark:*:first:before:block *:[[data-slot]~[data-slot]]:rounded-t-none *:[[data-slot]~[data-slot]]:border-t-0 *:[[data-slot]~[data-slot]]:before:rounded-t-none",
22
+ },
23
+ },
24
+ },
25
+ );
26
+
27
+ function Group({
28
+ className,
29
+ orientation,
30
+ children,
31
+ ...props
32
+ }: {
33
+ className?: string;
34
+ orientation?: VariantProps<typeof groupVariants>["orientation"];
35
+ children: React.ReactNode;
36
+ } & React.ComponentProps<"div">) {
37
+ return (
38
+ // biome-ignore lint/a11y/useSemanticElements: Imported from library
39
+ <div
40
+ className={cn(groupVariants({ orientation }), className)}
41
+ data-orientation={orientation}
42
+ data-slot="group"
43
+ role="group"
44
+ {...props}
45
+ >
46
+ {children}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ function GroupText({
52
+ className,
53
+ render,
54
+ ...props
55
+ }: useRender.ComponentProps<"div">) {
56
+ const defaultProps = {
57
+ className: cn(
58
+ "relative inline-flex items-center whitespace-nowrap gap-2 rounded-lg border border-input bg-muted not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-muted-foreground text-base sm:text-sm shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 [&_svg]:-mx-0.5",
59
+ className,
60
+ ),
61
+ "data-slot": "group-text",
62
+ };
63
+ return useRender({
64
+ defaultTagName: "div",
65
+ props: mergeProps(defaultProps, props),
66
+ render,
67
+ });
68
+ }
69
+
70
+ function GroupSeparator({
71
+ className,
72
+ orientation = "vertical",
73
+ ...props
74
+ }: {
75
+ className?: string;
76
+ } & React.ComponentProps<typeof Separator>) {
77
+ return (
78
+ <Separator
79
+ className={cn(
80
+ "[[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+input+&]:-translate-x-px pointer-events-none relative z-2 bg-input before:absolute before:inset-0 has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:translate-x-px has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:bg-ring dark:before:bg-input/32 [[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+&,[data-slot=number-field]:focus-within+input+&]:bg-ring",
81
+ className,
82
+ )}
83
+ orientation={orientation}
84
+ {...props}
85
+ />
86
+ );
87
+ }
88
+
89
+ export {
90
+ Group,
91
+ Group as ButtonGroup,
92
+ GroupText,
93
+ GroupText as ButtonGroupText,
94
+ GroupSeparator,
95
+ GroupSeparator as ButtonGroupSeparator,
96
+ groupVariants,
97
+ };
@@ -0,0 +1,228 @@
1
+ "use client";
2
+
3
+ import { Transition } from "@headlessui/react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import type React from "react";
6
+ import {
7
+ createContext,
8
+ forwardRef,
9
+ useContext,
10
+ useEffect,
11
+ useMemo,
12
+ useRef,
13
+ useState,
14
+ } from "react";
15
+ import useResizeObserver from "use-resize-observer";
16
+ import useCallbackRef from "../../lib/hooks/use-callback-ref";
17
+ import { useHover } from "../../lib/hooks/use-hover";
18
+ import { cn } from "../../lib/utils/css";
19
+ import type { AsChildProps } from "../../types/helpers";
20
+
21
+ interface HorizontalScrollFaderContextValue {
22
+ isLeftOverflowing: boolean;
23
+ isRightOverflowing: boolean;
24
+ containerRef: React.RefObject<HTMLElement | null>;
25
+ onScroll: () => void;
26
+ }
27
+
28
+ const HorizontalScrollFaderContext =
29
+ createContext<HorizontalScrollFaderContextValue>({
30
+ isLeftOverflowing: false,
31
+ isRightOverflowing: false,
32
+ containerRef: { current: null },
33
+ onScroll: () => {},
34
+ });
35
+
36
+ function HorizontalScrollFaderInner(
37
+ { asChild, ...props }: AsChildProps<"div">,
38
+ ref: React.ForwardedRef<HTMLDivElement>,
39
+ ) {
40
+ const containerRef = useRef<HTMLElement>(null);
41
+
42
+ const [isLeftOverflowing, setIsLeftOverflowing] = useState(false);
43
+ const [isRightOverflowing, setIsRightOverflowing] = useState(false);
44
+
45
+ const onScroll = useCallbackRef(() => {
46
+ if (!containerRef.current) return;
47
+
48
+ const { scrollLeft, scrollWidth, clientWidth } = containerRef.current;
49
+ setIsLeftOverflowing(scrollLeft > 0);
50
+ setIsRightOverflowing(scrollLeft + clientWidth < scrollWidth);
51
+ });
52
+
53
+ const contextValue = useMemo(
54
+ () => ({ isLeftOverflowing, isRightOverflowing, containerRef, onScroll }),
55
+ [isLeftOverflowing, isRightOverflowing, onScroll],
56
+ );
57
+
58
+ const Comp = asChild ? Slot : "div";
59
+
60
+ return (
61
+ <HorizontalScrollFaderContext.Provider value={contextValue}>
62
+ <Slot className="relative" onScroll={onScroll}>
63
+ <Comp ref={ref} {...props} />
64
+ </Slot>
65
+ </HorizontalScrollFaderContext.Provider>
66
+ );
67
+ }
68
+
69
+ const HorizontalScrollFader = forwardRef(HorizontalScrollFaderInner);
70
+
71
+ function HorizontalScrollFaderContentInner(
72
+ { asChild, ...props }: AsChildProps<"div">,
73
+ ref: React.ForwardedRef<HTMLDivElement>,
74
+ ) {
75
+ const Comp = asChild ? Slot : "div";
76
+ const { isLeftOverflowing, isRightOverflowing, containerRef, onScroll } =
77
+ useContext(HorizontalScrollFaderContext);
78
+
79
+ useResizeObserver({
80
+ ref: containerRef as React.RefObject<HTMLElement>,
81
+ onResize: onScroll,
82
+ });
83
+
84
+ // A single horizontal mask gradient fades each edge over its fader size.
85
+ // When an edge isn't overflowing its fader size is 0, so no fade shows.
86
+ // Inline styles (with the -webkit- prefix for Safari) keep this independent
87
+ // of Tailwind mask-utility emission.
88
+ const maskImage = `linear-gradient(to right, transparent 0, #000 var(--left-fader-size), #000 calc(100% - var(--right-fader-size)), transparent 100%)`;
89
+
90
+ return (
91
+ <Slot
92
+ onScroll={onScroll}
93
+ className={cn(
94
+ "overflow-auto [transition:--left-fader-size_150ms,--right-fader-size_150ms] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
95
+ )}
96
+ style={{
97
+ "--left-fader-size": isLeftOverflowing ? "3rem" : "0rem",
98
+ "--right-fader-size": isRightOverflowing ? "3rem" : "0rem",
99
+ maskImage,
100
+ WebkitMaskImage: maskImage,
101
+ }}
102
+ ref={containerRef}
103
+ >
104
+ <Comp ref={ref} {...props} />
105
+ </Slot>
106
+ );
107
+ }
108
+
109
+ const HorizontalScrollFaderContent = forwardRef(
110
+ HorizontalScrollFaderContentInner,
111
+ );
112
+
113
+ function HorizontalScrollFaderLeftScrollerInner(
114
+ { asChild, ...props }: AsChildProps<"button">,
115
+ ref: React.ForwardedRef<HTMLButtonElement>,
116
+ ) {
117
+ const Comp = asChild ? Slot : "button";
118
+ const { containerRef, isLeftOverflowing } = useContext(
119
+ HorizontalScrollFaderContext,
120
+ );
121
+ const onClick = useCallbackRef(() => {
122
+ if (!containerRef.current) return;
123
+ containerRef.current.scrollBy({
124
+ left: -containerRef.current.clientWidth,
125
+ behavior: "smooth",
126
+ });
127
+ });
128
+
129
+ const [hoverRef, hovered] = useHover<HTMLElement>({
130
+ mounted: isLeftOverflowing,
131
+ });
132
+ useEffect(() => {
133
+ if (!hovered || !isLeftOverflowing) return;
134
+ let stopped = false;
135
+ let animationFrameId: number;
136
+ let startTime: DOMHighResTimeStamp;
137
+
138
+ const animate = (timestamp: DOMHighResTimeStamp) => {
139
+ if (stopped) return;
140
+ if (!startTime) startTime = timestamp;
141
+ const elapsed = Math.min(timestamp - startTime, 2000);
142
+ containerRef.current?.scrollBy({
143
+ left: -Math.log2(elapsed + 1),
144
+ });
145
+ animationFrameId = requestAnimationFrame(animate);
146
+ };
147
+ animationFrameId = requestAnimationFrame(animate);
148
+
149
+ return () => {
150
+ stopped = true;
151
+ cancelAnimationFrame(animationFrameId);
152
+ };
153
+ }, [hovered, isLeftOverflowing, containerRef.current?.scrollBy]);
154
+
155
+ return (
156
+ <Transition show={isLeftOverflowing}>
157
+ <Slot onClick={onClick} ref={hoverRef}>
158
+ <Comp {...(asChild ? {} : { type: "button" })} ref={ref} {...props} />
159
+ </Slot>
160
+ </Transition>
161
+ );
162
+ }
163
+
164
+ const HorizontalScrollFaderLeftScroller = forwardRef(
165
+ HorizontalScrollFaderLeftScrollerInner,
166
+ );
167
+
168
+ function HorizontalScrollFaderRightScrollerInner(
169
+ { asChild, ...props }: AsChildProps<"button">,
170
+ ref: React.ForwardedRef<HTMLButtonElement>,
171
+ ) {
172
+ const Comp = asChild ? Slot : "button";
173
+ const { containerRef, isRightOverflowing } = useContext(
174
+ HorizontalScrollFaderContext,
175
+ );
176
+ const onClick = useCallbackRef(() => {
177
+ if (!containerRef.current) return;
178
+ containerRef.current.scrollBy({
179
+ left: containerRef.current.clientWidth,
180
+ behavior: "smooth",
181
+ });
182
+ });
183
+
184
+ const [hoverRef, hovered] = useHover({
185
+ mounted: isRightOverflowing,
186
+ });
187
+ useEffect(() => {
188
+ if (!hovered || !isRightOverflowing) return;
189
+ let stopped = false;
190
+ let animationFrameId: number;
191
+ let startTime: DOMHighResTimeStamp;
192
+
193
+ const animate = (timestamp: DOMHighResTimeStamp) => {
194
+ if (stopped) return;
195
+ if (!startTime) startTime = timestamp;
196
+ const elapsed = Math.min(timestamp - startTime, 2000);
197
+ containerRef.current?.scrollBy({
198
+ left: Math.log2(elapsed + 1),
199
+ });
200
+ animationFrameId = requestAnimationFrame(animate);
201
+ };
202
+ animationFrameId = requestAnimationFrame(animate);
203
+
204
+ return () => {
205
+ stopped = true;
206
+ cancelAnimationFrame(animationFrameId);
207
+ };
208
+ }, [hovered, isRightOverflowing, containerRef.current?.scrollBy]);
209
+
210
+ return (
211
+ <Transition show={isRightOverflowing}>
212
+ <Slot onClick={onClick} ref={hoverRef}>
213
+ <Comp {...(asChild ? {} : { type: "button" })} ref={ref} {...props} />
214
+ </Slot>
215
+ </Transition>
216
+ );
217
+ }
218
+
219
+ const HorizontalScrollFaderRightScroller = forwardRef(
220
+ HorizontalScrollFaderRightScrollerInner,
221
+ );
222
+
223
+ export {
224
+ HorizontalScrollFader,
225
+ HorizontalScrollFaderContent,
226
+ HorizontalScrollFaderLeftScroller,
227
+ HorizontalScrollFaderRightScroller,
228
+ };
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import type * as React from "react";
5
+ import { Input, type InputProps } from "./input";
6
+ import { Textarea, type TextareaProps } from "./textarea";
7
+ import { cn } from "../../lib/utils/css";
8
+
9
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10
+ return (
11
+ // biome-ignore lint/a11y/useSemanticElements: Imported from library
12
+ <div
13
+ className={cn(
14
+ "relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/6%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
15
+ className,
16
+ )}
17
+ data-slot="input-group"
18
+ role="group"
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ const inputGroupAddonVariants = cva(
25
+ "[&_svg]:-mx-0.5 flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
26
+ {
27
+ defaultVariants: {
28
+ align: "inline-start",
29
+ },
30
+ variants: {
31
+ align: {
32
+ "block-end":
33
+ "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
34
+ "block-start":
35
+ "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
36
+ "inline-end":
37
+ "has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
38
+ "inline-start":
39
+ "has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
40
+ },
41
+ },
42
+ },
43
+ );
44
+
45
+ function InputGroupAddon({
46
+ className,
47
+ align = "inline-start",
48
+ ...props
49
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
50
+ return (
51
+ // biome-ignore lint/a11y/noStaticElementInteractions: Imported from library
52
+ <div
53
+ className={cn(inputGroupAddonVariants({ align }), className)}
54
+ data-align={align}
55
+ data-slot="input-group-addon"
56
+ onMouseDown={(e) => {
57
+ const target = e.target as HTMLElement;
58
+ const isInteractive = target.closest(
59
+ "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
60
+ );
61
+ if (isInteractive) return;
62
+ e.preventDefault();
63
+ const parent = e.currentTarget.parentElement;
64
+ const input = parent?.querySelector<
65
+ HTMLInputElement | HTMLTextAreaElement
66
+ >("input, textarea");
67
+ if (input && !parent?.querySelector("input:focus, textarea:focus")) {
68
+ input.focus();
69
+ }
70
+ }}
71
+ {...props}
72
+ />
73
+ );
74
+ }
75
+
76
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
77
+ return (
78
+ <span
79
+ className={cn(
80
+ "[&_svg]:-mx-0.5 line-clamp-1 flex items-center gap-2 text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
81
+ className,
82
+ )}
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ function InputGroupInput({ className, ...props }: InputProps) {
89
+ return <Input className={className} unstyled {...props} />;
90
+ }
91
+
92
+ function InputGroupTextarea({ className, ...props }: TextareaProps) {
93
+ return <Textarea className={className} unstyled {...props} />;
94
+ }
95
+
96
+ export {
97
+ InputGroup,
98
+ InputGroupAddon,
99
+ InputGroupText,
100
+ InputGroupInput,
101
+ InputGroupTextarea,
102
+ };
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { OTPInput, OTPInputContext, REGEXP_ONLY_DIGITS } from "input-otp";
5
+ import { MinusIcon } from "lucide-react";
6
+ import * as React from "react";
7
+
8
+ import { cn } from "../../lib/utils/css";
9
+
10
+ type InputOTPProps = React.ComponentProps<typeof OTPInput>;
11
+
12
+ function InputOTP({
13
+ className,
14
+ containerClassName,
15
+ // Digits-only by default. Override `pattern`/`inputMode` to allow letters.
16
+ inputMode = "numeric",
17
+ pattern = REGEXP_ONLY_DIGITS,
18
+ ...props
19
+ }: InputOTPProps) {
20
+ return (
21
+ <OTPInput
22
+ data-slot="input-otp"
23
+ containerClassName={cn(
24
+ "flex items-center gap-2 has-disabled:opacity-50",
25
+ containerClassName,
26
+ )}
27
+ className={cn("disabled:cursor-not-allowed", className)}
28
+ inputMode={inputMode}
29
+ pattern={pattern}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
36
+ return (
37
+ <div
38
+ data-slot="input-otp-group"
39
+ className={cn("flex items-center", className)}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ const inputOTPSlotVariants = cva(
46
+ "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex items-center justify-center border-y border-r shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
47
+ {
48
+ variants: {
49
+ size: {
50
+ sm: "h-8 w-8 text-xs",
51
+ default: "h-9 w-9 text-sm",
52
+ lg: "h-11 w-11 text-base",
53
+ },
54
+ },
55
+ defaultVariants: { size: "default" },
56
+ },
57
+ );
58
+
59
+ function InputOTPSlot({
60
+ index,
61
+ className,
62
+ size,
63
+ ...props
64
+ }: React.ComponentProps<"div"> & {
65
+ index: number;
66
+ } & VariantProps<typeof inputOTPSlotVariants>) {
67
+ const inputOTPContext = React.useContext(OTPInputContext);
68
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
69
+
70
+ return (
71
+ <div
72
+ data-slot="input-otp-slot"
73
+ data-active={isActive}
74
+ className={cn(inputOTPSlotVariants({ size }), className)}
75
+ {...props}
76
+ >
77
+ {char}
78
+ {hasFakeCaret && (
79
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
80
+ <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
81
+ </div>
82
+ )}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
88
+ return (
89
+ // biome-ignore lint/a11y/useFocusableInteractive lint/a11y/useSemanticElements lint/a11y/useAriaPropsForRole: Installed from shadcn
90
+ <div data-slot="input-otp-separator" role="separator" {...props}>
91
+ <MinusIcon />
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };