@kinetixui/ui 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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +1176 -0
  3. package/dist/index.js +3939 -0
  4. package/package.json +106 -0
  5. package/src/components/accordion.tsx +52 -0
  6. package/src/components/alert-dialog.tsx +115 -0
  7. package/src/components/alert.tsx +47 -0
  8. package/src/components/aspect-ratio.tsx +7 -0
  9. package/src/components/audio-player.tsx +150 -0
  10. package/src/components/avatar.tsx +76 -0
  11. package/src/components/badge.tsx +40 -0
  12. package/src/components/breadcrumb.tsx +77 -0
  13. package/src/components/button.tsx +119 -0
  14. package/src/components/calendar.tsx +60 -0
  15. package/src/components/card.tsx +50 -0
  16. package/src/components/carousel.tsx +181 -0
  17. package/src/components/chart.tsx +177 -0
  18. package/src/components/checkbox.tsx +38 -0
  19. package/src/components/circular-progress.tsx +67 -0
  20. package/src/components/code-block.tsx +96 -0
  21. package/src/components/collapsible.tsx +9 -0
  22. package/src/components/command.tsx +123 -0
  23. package/src/components/context-menu.tsx +132 -0
  24. package/src/components/data-table.tsx +92 -0
  25. package/src/components/date-picker.tsx +68 -0
  26. package/src/components/dialog.tsx +101 -0
  27. package/src/components/drawer.tsx +82 -0
  28. package/src/components/dropdown-menu.tsx +132 -0
  29. package/src/components/fab.tsx +58 -0
  30. package/src/components/field.tsx +128 -0
  31. package/src/components/file-upload.tsx +183 -0
  32. package/src/components/footer.tsx +62 -0
  33. package/src/components/form.tsx +130 -0
  34. package/src/components/hover-card.tsx +28 -0
  35. package/src/components/image.tsx +87 -0
  36. package/src/components/inform.tsx +82 -0
  37. package/src/components/input-group.tsx +96 -0
  38. package/src/components/input-otp.tsx +63 -0
  39. package/src/components/input.tsx +72 -0
  40. package/src/components/label.tsx +20 -0
  41. package/src/components/list.tsx +69 -0
  42. package/src/components/menubar.tsx +168 -0
  43. package/src/components/metric.tsx +51 -0
  44. package/src/components/modal.tsx +126 -0
  45. package/src/components/navigation-bar.tsx +49 -0
  46. package/src/components/navigation-menu.tsx +117 -0
  47. package/src/components/number-input.tsx +86 -0
  48. package/src/components/pagination.tsx +77 -0
  49. package/src/components/password-input.tsx +36 -0
  50. package/src/components/popover.tsx +32 -0
  51. package/src/components/progress.tsx +24 -0
  52. package/src/components/quote.tsx +34 -0
  53. package/src/components/radio-group.tsx +44 -0
  54. package/src/components/rating.tsx +88 -0
  55. package/src/components/resizable.tsx +40 -0
  56. package/src/components/scroll-area.tsx +39 -0
  57. package/src/components/select.tsx +124 -0
  58. package/src/components/separator.tsx +25 -0
  59. package/src/components/sheet.tsx +104 -0
  60. package/src/components/sidebar.tsx +390 -0
  61. package/src/components/skeleton.tsx +9 -0
  62. package/src/components/slider.tsx +24 -0
  63. package/src/components/sonner.tsx +30 -0
  64. package/src/components/spinner.tsx +43 -0
  65. package/src/components/stepper.tsx +75 -0
  66. package/src/components/switch.tsx +37 -0
  67. package/src/components/tab-bar.tsx +58 -0
  68. package/src/components/table-of-contents.tsx +46 -0
  69. package/src/components/table.tsx +70 -0
  70. package/src/components/tabs.tsx +54 -0
  71. package/src/components/tag.tsx +56 -0
  72. package/src/components/textarea.tsx +56 -0
  73. package/src/components/toggle-group.tsx +41 -0
  74. package/src/components/toggle.tsx +34 -0
  75. package/src/components/tooltip.tsx +31 -0
  76. package/src/index.ts +305 -0
  77. package/src/lib/utils.ts +7 -0
  78. package/src/stories/Button.stories.tsx +155 -0
  79. package/src/stories/Input.stories.tsx +80 -0
  80. package/src/stories/Textarea.stories.tsx +69 -0
  81. package/tailwind.config.ts +107 -0
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@kinetixui/ui",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
6
+ "keywords": [
7
+ "react",
8
+ "components",
9
+ "ui",
10
+ "design-system",
11
+ "radix-ui",
12
+ "tailwindcss",
13
+ "kinetixui"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "KinetixUI",
17
+ "homepage": "https://kinetixui.com",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/ziadfteha/kinetixui.git",
21
+ "directory": "packages/ui"
22
+ },
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./tailwind.config": "./tailwind.config.ts",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "tailwind.config.ts"
40
+ ],
41
+ "dependencies": {
42
+ "@hookform/resolvers": "^3.9.1",
43
+ "@radix-ui/react-accordion": "^1.2.2",
44
+ "@radix-ui/react-alert-dialog": "^1.1.4",
45
+ "@radix-ui/react-aspect-ratio": "^1.1.1",
46
+ "@radix-ui/react-avatar": "^1.1.2",
47
+ "@radix-ui/react-checkbox": "^1.1.3",
48
+ "@radix-ui/react-collapsible": "^1.1.2",
49
+ "@radix-ui/react-context-menu": "^2.2.4",
50
+ "@radix-ui/react-dialog": "^1.1.4",
51
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
52
+ "@radix-ui/react-hover-card": "^1.1.4",
53
+ "@radix-ui/react-label": "^2.1.1",
54
+ "@radix-ui/react-menubar": "^1.1.4",
55
+ "@radix-ui/react-navigation-menu": "^1.2.3",
56
+ "@radix-ui/react-popover": "^1.1.4",
57
+ "@radix-ui/react-progress": "^1.1.1",
58
+ "@radix-ui/react-radio-group": "^1.2.2",
59
+ "@radix-ui/react-scroll-area": "^1.2.2",
60
+ "@radix-ui/react-select": "^2.1.4",
61
+ "@radix-ui/react-separator": "^1.1.1",
62
+ "@radix-ui/react-slider": "^1.2.2",
63
+ "@radix-ui/react-slot": "^1.1.1",
64
+ "@radix-ui/react-switch": "^1.1.2",
65
+ "@radix-ui/react-tabs": "^1.1.2",
66
+ "@radix-ui/react-toggle": "^1.1.1",
67
+ "@radix-ui/react-toggle-group": "^1.1.1",
68
+ "@radix-ui/react-tooltip": "^1.1.6",
69
+ "@tanstack/react-table": "^8.20.6",
70
+ "class-variance-authority": "^0.7.1",
71
+ "clsx": "^2.1.1",
72
+ "cmdk": "^1.0.4",
73
+ "date-fns": "^3.6.0",
74
+ "embla-carousel-react": "^8.5.1",
75
+ "input-otp": "^1.4.1",
76
+ "lucide-react": "^0.468.0",
77
+ "react-day-picker": "8.10.1",
78
+ "react-hook-form": "^7.54.1",
79
+ "react-resizable-panels": "^2.1.7",
80
+ "recharts": "^2.15.0",
81
+ "sonner": "^1.7.1",
82
+ "tailwind-merge": "^2.6.0",
83
+ "tailwindcss-animate": "^1.0.7",
84
+ "vaul": "^1.1.2",
85
+ "zod": "^3.24.1"
86
+ },
87
+ "peerDependencies": {
88
+ "react": ">=18",
89
+ "react-dom": ">=18"
90
+ },
91
+ "devDependencies": {
92
+ "@kinetixui/tokens": "0.1.0",
93
+ "@types/react": "^18.3.17",
94
+ "@types/react-dom": "^18.3.5",
95
+ "react": "^18.3.1",
96
+ "react-dom": "^18.3.1",
97
+ "tailwindcss": "^3.4.17",
98
+ "tsup": "^8.3.5",
99
+ "typescript": "^5.7.2"
100
+ },
101
+ "scripts": {
102
+ "build": "tsup src/index.ts --format esm --dts --external react --external react-dom",
103
+ "lint": "eslint src",
104
+ "typecheck": "tsc --noEmit"
105
+ }
106
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
5
+ import { ChevronDown } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const Accordion = AccordionPrimitive.Root;
9
+
10
+ const AccordionItem = React.forwardRef<
11
+ React.ElementRef<typeof AccordionPrimitive.Item>,
12
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
13
+ >(({ className, ...props }, ref) => (
14
+ <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
15
+ ));
16
+ AccordionItem.displayName = "AccordionItem";
17
+
18
+ const AccordionTrigger = React.forwardRef<
19
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
20
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
21
+ >(({ className, children, ...props }, ref) => (
22
+ <AccordionPrimitive.Header className="flex">
23
+ <AccordionPrimitive.Trigger
24
+ ref={ref}
25
+ className={cn(
26
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium font-sans transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ <ChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
33
+ </AccordionPrimitive.Trigger>
34
+ </AccordionPrimitive.Header>
35
+ ));
36
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
37
+
38
+ const AccordionContent = React.forwardRef<
39
+ React.ElementRef<typeof AccordionPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
41
+ >(({ className, children, ...props }, ref) => (
42
+ <AccordionPrimitive.Content
43
+ ref={ref}
44
+ className="overflow-hidden text-sm font-sans data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
45
+ {...props}
46
+ >
47
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
48
+ </AccordionPrimitive.Content>
49
+ ));
50
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
51
+
52
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5
+ import { cn } from "../lib/utils";
6
+ import { buttonVariants } from "./button";
7
+
8
+ const AlertDialog = AlertDialogPrimitive.Root;
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
11
+
12
+ const AlertDialogOverlay = React.forwardRef<
13
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
14
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
15
+ >(({ className, ...props }, ref) => (
16
+ <AlertDialogPrimitive.Overlay
17
+ ref={ref}
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ ));
25
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
26
+
27
+ const AlertDialogContent = React.forwardRef<
28
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
29
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
30
+ >(({ className, ...props }, ref) => (
31
+ <AlertDialogPortal>
32
+ <AlertDialogOverlay />
33
+ <AlertDialogPrimitive.Content
34
+ ref={ref}
35
+ className={cn(
36
+ "fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
37
+ "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",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ));
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
+
46
+ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
48
+ );
49
+ AlertDialogHeader.displayName = "AlertDialogHeader";
50
+
51
+ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
54
+ {...props}
55
+ />
56
+ );
57
+ AlertDialogFooter.displayName = "AlertDialogFooter";
58
+
59
+ const AlertDialogTitle = React.forwardRef<
60
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
61
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
62
+ >(({ className, ...props }, ref) => (
63
+ <AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
64
+ ));
65
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
66
+
67
+ const AlertDialogDescription = React.forwardRef<
68
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
69
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
70
+ >(({ className, ...props }, ref) => (
71
+ <AlertDialogPrimitive.Description
72
+ ref={ref}
73
+ className={cn("text-sm text-muted-foreground", className)}
74
+ {...props}
75
+ />
76
+ ));
77
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
78
+
79
+ const AlertDialogAction = React.forwardRef<
80
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
81
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
82
+ >(({ className, ...props }, ref) => (
83
+ <AlertDialogPrimitive.Action
84
+ ref={ref}
85
+ className={cn(buttonVariants({ variant: "Primary", size: "md" }), className)}
86
+ {...props}
87
+ />
88
+ ));
89
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
90
+
91
+ const AlertDialogCancel = React.forwardRef<
92
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
93
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
94
+ >(({ className, ...props }, ref) => (
95
+ <AlertDialogPrimitive.Cancel
96
+ ref={ref}
97
+ className={cn(buttonVariants({ variant: "Outline", size: "md" }), "mt-2 sm:mt-0", className)}
98
+ {...props}
99
+ />
100
+ ));
101
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
102
+
103
+ export {
104
+ AlertDialog,
105
+ AlertDialogPortal,
106
+ AlertDialogOverlay,
107
+ AlertDialogTrigger,
108
+ AlertDialogContent,
109
+ AlertDialogHeader,
110
+ AlertDialogFooter,
111
+ AlertDialogTitle,
112
+ AlertDialogDescription,
113
+ AlertDialogAction,
114
+ AlertDialogCancel,
115
+ };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const alertVariants = cva(
8
+ "relative w-full rounded-lg border px-4 py-3 text-sm font-sans [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-background text-foreground",
13
+ destructive:
14
+ "border-destructive/50 text-destructive [&>svg]:text-destructive",
15
+ success: "border-success/50 text-success [&>svg]:text-success",
16
+ warning: "border-warning/50 text-warning [&>svg]:text-warning",
17
+ info: "border-info/50 text-info [&>svg]:text-info",
18
+ },
19
+ },
20
+ defaultVariants: { variant: "default" },
21
+ },
22
+ );
23
+
24
+ const Alert = React.forwardRef<
25
+ HTMLDivElement,
26
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
27
+ >(({ className, variant, ...props }, ref) => (
28
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
29
+ ));
30
+ Alert.displayName = "Alert";
31
+
32
+ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
33
+ ({ className, ...props }, ref) => (
34
+ <h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
35
+ ),
36
+ );
37
+ AlertTitle.displayName = "AlertTitle";
38
+
39
+ const AlertDescription = React.forwardRef<
40
+ HTMLParagraphElement,
41
+ React.HTMLAttributes<HTMLParagraphElement>
42
+ >(({ className, ...props }, ref) => (
43
+ <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
44
+ ));
45
+ AlertDescription.displayName = "AlertDescription";
46
+
47
+ export { Alert, AlertTitle, AlertDescription };
@@ -0,0 +1,7 @@
1
+ "use client";
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
4
+
5
+ const AspectRatio = AspectRatioPrimitive.Root;
6
+
7
+ export { AspectRatio };
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Pause, Play, SkipBack, SkipForward } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+ import { Slider } from "./slider";
7
+
8
+ /**
9
+ * AudioPlayer — interactive playback controls, full or mini. Wraps a native
10
+ * <audio> element: play / pause, scrubber with time, ±10s skip, and optional
11
+ * prev / next track callbacks.
12
+ */
13
+ export interface AudioPlayerProps extends React.HTMLAttributes<HTMLDivElement> {
14
+ src: string;
15
+ variant?: "full" | "mini";
16
+ title?: string;
17
+ artist?: string;
18
+ coverSrc?: string;
19
+ /** ±10s skip step, seconds */
20
+ skipBy?: number;
21
+ onPrev?: () => void;
22
+ onNext?: () => void;
23
+ }
24
+
25
+ function fmt(t: number) {
26
+ if (!Number.isFinite(t)) return "0:00";
27
+ const m = Math.floor(t / 60);
28
+ const s = Math.floor(t % 60);
29
+ return `${m}:${s.toString().padStart(2, "0")}`;
30
+ }
31
+
32
+ const AudioPlayer = React.forwardRef<HTMLDivElement, AudioPlayerProps>(
33
+ ({ src, variant = "full", title, artist, coverSrc, skipBy = 10, onPrev, onNext, className, ...props }, ref) => {
34
+ const audioRef = React.useRef<HTMLAudioElement>(null);
35
+ const [playing, setPlaying] = React.useState(false);
36
+ const [current, setCurrent] = React.useState(0);
37
+ const [duration, setDuration] = React.useState(0);
38
+
39
+ const toggle = () => {
40
+ const a = audioRef.current;
41
+ if (!a) return;
42
+ if (a.paused) void a.play();
43
+ else a.pause();
44
+ };
45
+ const seek = (t: number) => {
46
+ const a = audioRef.current;
47
+ if (a) a.currentTime = Math.max(0, Math.min(duration || 0, t));
48
+ };
49
+ const skip = (delta: number) => seek(current + delta);
50
+
51
+ const audio = (
52
+ <audio
53
+ ref={audioRef}
54
+ src={src}
55
+ preload="metadata"
56
+ onPlay={() => setPlaying(true)}
57
+ onPause={() => setPlaying(false)}
58
+ onTimeUpdate={(e) => setCurrent(e.currentTarget.currentTime)}
59
+ onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}
60
+ onEnded={() => setPlaying(false)}
61
+ />
62
+ );
63
+
64
+ const PlayPause = ({ size = 44 }: { size?: number }) => (
65
+ <button
66
+ type="button"
67
+ onClick={toggle}
68
+ aria-label={playing ? "Pause" : "Play"}
69
+ className="grid shrink-0 place-items-center rounded-full bg-primary text-primary-foreground outline-none transition-transform hover:scale-105 focus-visible:shadow-focus"
70
+ style={{ width: size, height: size }}
71
+ >
72
+ {playing ? <Pause className="size-1/3 fill-current" /> : <Play className="size-1/3 translate-x-px fill-current" />}
73
+ </button>
74
+ );
75
+
76
+ const iconBtn =
77
+ "grid size-8 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:text-foreground disabled:opacity-40 [&>svg]:size-4";
78
+
79
+ if (variant === "mini") {
80
+ const pct = duration ? (current / duration) * 100 : 0;
81
+ return (
82
+ <div
83
+ ref={ref}
84
+ className={cn(
85
+ "relative flex w-full max-w-[360px] items-center gap-3 overflow-hidden rounded-full border border-border bg-background p-2 pl-3 font-sans",
86
+ className,
87
+ )}
88
+ {...props}
89
+ >
90
+ {audio}
91
+ {coverSrc ? (
92
+ <img src={coverSrc} alt="" className="size-10 shrink-0 rounded-full object-cover" />
93
+ ) : (
94
+ <div className="size-10 shrink-0 rounded-full bg-muted" />
95
+ )}
96
+ <div className="min-w-0 flex-1">
97
+ <p className="truncate text-label-md font-semibold text-foreground">{title ?? "Audio"}</p>
98
+ {artist && <p className="truncate text-body-sm text-muted-foreground">{artist}</p>}
99
+ </div>
100
+ <button type="button" onClick={() => skip(skipBy)} aria-label={`Forward ${skipBy}s`} className={iconBtn}>
101
+ <SkipForward />
102
+ </button>
103
+ <PlayPause size={36} />
104
+ <div className="absolute inset-x-0 bottom-0 h-1 bg-muted">
105
+ <div className="h-full bg-primary transition-[width]" style={{ width: `${pct}%` }} />
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ return (
112
+ <div
113
+ ref={ref}
114
+ className={cn("flex w-full max-w-[360px] flex-col gap-2 font-sans", className)}
115
+ {...props}
116
+ >
117
+ {audio}
118
+ <Slider
119
+ value={[current]}
120
+ max={duration || 1}
121
+ step={0.5}
122
+ onValueChange={(vals) => seek(vals[0] ?? 0)}
123
+ aria-label="Seek"
124
+ />
125
+ <div className="flex justify-between text-body-sm text-muted-foreground">
126
+ <span>{fmt(current)}</span>
127
+ <span>{fmt(duration)}</span>
128
+ </div>
129
+ <div className="mt-1 flex items-center justify-center gap-3">
130
+ <button type="button" onClick={onPrev} disabled={!onPrev} aria-label="Previous track" className={iconBtn}>
131
+ <SkipBack className="fill-current" />
132
+ </button>
133
+ <button type="button" onClick={() => skip(-skipBy)} aria-label={`Back ${skipBy}s`} className={iconBtn}>
134
+ <SkipBack />
135
+ </button>
136
+ <PlayPause />
137
+ <button type="button" onClick={() => skip(skipBy)} aria-label={`Forward ${skipBy}s`} className={iconBtn}>
138
+ <SkipForward />
139
+ </button>
140
+ <button type="button" onClick={onNext} disabled={!onNext} aria-label="Next track" className={iconBtn}>
141
+ <SkipForward className="fill-current" />
142
+ </button>
143
+ </div>
144
+ </div>
145
+ );
146
+ },
147
+ );
148
+ AudioPlayer.displayName = "AudioPlayer";
149
+
150
+ export { AudioPlayer };
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const Avatar = React.forwardRef<
8
+ React.ElementRef<typeof AvatarPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <AvatarPrimitive.Root
12
+ ref={ref}
13
+ className={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
14
+ {...props}
15
+ />
16
+ ));
17
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
18
+
19
+ const AvatarImage = React.forwardRef<
20
+ React.ElementRef<typeof AvatarPrimitive.Image>,
21
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
22
+ >(({ className, ...props }, ref) => (
23
+ <AvatarPrimitive.Image ref={ref} className={cn("aspect-square size-full", className)} {...props} />
24
+ ));
25
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
26
+
27
+ const AvatarFallback = React.forwardRef<
28
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
29
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
30
+ >(({ className, ...props }, ref) => (
31
+ <AvatarPrimitive.Fallback
32
+ ref={ref}
33
+ className={cn(
34
+ "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground",
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ ));
40
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
41
+
42
+ /**
43
+ * AvatarGroup — stacked, overlapping avatars with a "+N" overflow marker.
44
+ */
45
+ export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
46
+ /** cap the number of visible avatars, folding the rest into a "+N" marker */
47
+ max?: number;
48
+ }
49
+
50
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
51
+ ({ className, max, children, ...props }, ref) => {
52
+ const items = React.Children.toArray(children);
53
+ const visible = max ? items.slice(0, max) : items;
54
+ const overflow = max ? items.length - max : 0;
55
+
56
+ return (
57
+ <div ref={ref} className={cn("flex -space-x-2", className)} {...props}>
58
+ {visible.map((child, i) => (
59
+ <div key={i} className="rounded-full ring-2 ring-background">
60
+ {child}
61
+ </div>
62
+ ))}
63
+ {overflow > 0 && (
64
+ <div className="rounded-full ring-2 ring-background">
65
+ <Avatar>
66
+ <AvatarFallback>+{overflow}</AvatarFallback>
67
+ </Avatar>
68
+ </div>
69
+ )}
70
+ </div>
71
+ );
72
+ },
73
+ );
74
+ AvatarGroup.displayName = "AvatarGroup";
75
+
76
+ export { Avatar, AvatarImage, AvatarFallback, AvatarGroup };
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Badge — reconciled 1:1 with the KinetixUI design source, node 54855:13995.
9
+ * Solid pill status marker. Label Medium type (12 / 16, +0.5). Variants:
10
+ * default | secondary | destructive | outline | subtle.
11
+ *
12
+ * Note: the design's "Secondary" badge is the saturated sage (`#748873`), which
13
+ * in this token contract is `--secondary-foreground`; `--secondary` is its light
14
+ * container. Hence the swapped bg/text on that one variant.
15
+ */
16
+ const badgeVariants = cva(
17
+ "inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium font-sans tracking-[0.5px] transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: "bg-primary text-primary-foreground",
22
+ secondary: "bg-secondary-foreground text-secondary",
23
+ destructive: "bg-destructive text-destructive-foreground",
24
+ outline: "border border-input text-foreground",
25
+ subtle: "bg-accent text-accent-foreground",
26
+ },
27
+ },
28
+ defaultVariants: { variant: "default" },
29
+ },
30
+ );
31
+
32
+ export interface BadgeProps
33
+ extends React.HTMLAttributes<HTMLDivElement>,
34
+ VariantProps<typeof badgeVariants> {}
35
+
36
+ function Badge({ className, variant, ...props }: BadgeProps) {
37
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
38
+ }
39
+
40
+ export { Badge, badgeVariants };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const Breadcrumb = React.forwardRef<HTMLElement, React.ComponentPropsWithoutRef<"nav">>(
9
+ ({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" className="font-sans" {...props} />,
10
+ );
11
+ Breadcrumb.displayName = "Breadcrumb";
12
+
13
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
14
+ ({ className, ...props }, ref) => (
15
+ <ol
16
+ ref={ref}
17
+ className={cn("flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", className)}
18
+ {...props}
19
+ />
20
+ ),
21
+ );
22
+ BreadcrumbList.displayName = "BreadcrumbList";
23
+
24
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
25
+ ({ className, ...props }, ref) => (
26
+ <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
27
+ ),
28
+ );
29
+ BreadcrumbItem.displayName = "BreadcrumbItem";
30
+
31
+ const BreadcrumbLink = React.forwardRef<
32
+ HTMLAnchorElement,
33
+ React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }
34
+ >(({ asChild, className, ...props }, ref) => {
35
+ const Comp = asChild ? Slot : "a";
36
+ return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
37
+ });
38
+ BreadcrumbLink.displayName = "BreadcrumbLink";
39
+
40
+ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
41
+ ({ className, ...props }, ref) => (
42
+ <span
43
+ ref={ref}
44
+ role="link"
45
+ aria-disabled="true"
46
+ aria-current="page"
47
+ className={cn("font-normal text-foreground", className)}
48
+ {...props}
49
+ />
50
+ ),
51
+ );
52
+ BreadcrumbPage.displayName = "BreadcrumbPage";
53
+
54
+ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
55
+ <li role="presentation" aria-hidden className={cn("[&>svg]:size-3.5", className)} {...props}>
56
+ {children ?? <ChevronRight />}
57
+ </li>
58
+ );
59
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
60
+
61
+ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
62
+ <span role="presentation" aria-hidden className={cn("flex size-9 items-center justify-center", className)} {...props}>
63
+ <MoreHorizontal className="size-4" />
64
+ <span className="sr-only">More</span>
65
+ </span>
66
+ );
67
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
68
+
69
+ export {
70
+ Breadcrumb,
71
+ BreadcrumbList,
72
+ BreadcrumbItem,
73
+ BreadcrumbLink,
74
+ BreadcrumbPage,
75
+ BreadcrumbSeparator,
76
+ BreadcrumbEllipsis,
77
+ };