diff --git a/apps/docs/src/examples/button.tsx b/apps/docs/src/examples/button.tsx
index 4cded248b..e84402541 100644
--- a/apps/docs/src/examples/button.tsx
+++ b/apps/docs/src/examples/button.tsx
@@ -1,37 +1,48 @@
+'use client';
+
import { Button } from '@vitnode/core/components/ui/button';
import { Card } from '@vitnode/core/components/ui/card';
import { ArrowRight, CheckCircle, Eye, Home, Star, Trash2 } from 'lucide-react';
+import React from 'react';
export default function ButtonExample() {
+ const [isLoading, setIsLoading] = React.useState(false);
+
return (
-
);
}
diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json
index af2dff761..855de85f1 100644
--- a/packages/vitnode/package.json
+++ b/packages/vitnode/package.json
@@ -32,6 +32,7 @@
"react": "19.1.x",
"react-dom": "19.1.x",
"react-hook-form": "^7.x.x",
+ "motion": "^12.x.x",
"typescript": "^5.8.x",
"zod": "4.x.x"
},
@@ -115,6 +116,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"input-otp": "^1.4.2",
+ "motion": "^12.23.6",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.5",
"postgres": "^3.4.7",
diff --git a/packages/vitnode/src/components/ui/button-client.tsx b/packages/vitnode/src/components/ui/button-client.tsx
new file mode 100644
index 000000000..7b74a49fc
--- /dev/null
+++ b/packages/vitnode/src/components/ui/button-client.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { AnimatePresence, motion } from 'motion/react';
+import { useTranslations } from 'next-intl';
+import { Slot } from 'radix-ui';
+
+import { cn } from '../../lib/utils';
+import { type ButtonProps, buttonVariants } from './button';
+import { Loader } from './loader';
+
+export function ClientButton({
+ className,
+ variant,
+ size,
+ asChild = false,
+ isLoading,
+ children,
+ ...props
+}: ButtonProps) {
+ const Comp = asChild ? Slot.Root : 'button';
+ const t = useTranslations('core.global');
+
+ return (
+
+
+
+ {children}
+
+
+
+ {isLoading && (
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/vitnode/src/components/ui/button.tsx b/packages/vitnode/src/components/ui/button.tsx
index 19ad46cf8..2422d5bb3 100644
--- a/packages/vitnode/src/components/ui/button.tsx
+++ b/packages/vitnode/src/components/ui/button.tsx
@@ -1,14 +1,10 @@
import { cva, type VariantProps } from 'class-variance-authority';
-import { useTranslations } from 'next-intl';
-import { Slot } from 'radix-ui';
import * as React from 'react';
-import { cn } from '@/lib/utils';
-
-import { Loader } from './loader';
+import { ClientButton } from './button-client';
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer overflow-hidden",
{
variants: {
variant: {
@@ -40,52 +36,17 @@ const buttonVariants = cva(
},
);
-type ButtonProps = React.ComponentProps<'button'> &
+export type ButtonProps = React.ComponentProps<'button'> &
VariantProps & {
asChild?: boolean;
isLoading?: boolean;
- loadingText?: string;
} & (
| { 'aria-label': string; size: 'icon' }
| { 'aria-label'?: string; size?: 'default' | 'lg' | 'sm' }
);
-function Button({
- className,
- variant,
- size,
- asChild = false,
- isLoading,
- loadingText,
- ...props
-}: ButtonProps) {
- const t = useTranslations('core.global');
- const Comp = asChild ? Slot.Root : 'button';
-
- if (isLoading) {
- const text = loadingText ?? t('loading');
-
- return (
-
-
- {size !== 'icon' && text}
-
- );
- }
-
- return (
-
- );
+function Button(props: ButtonProps) {
+ return ;
}
export { Button, buttonVariants };
diff --git a/packages/vitnode/src/components/ui/loader.tsx b/packages/vitnode/src/components/ui/loader.tsx
index 4e627cd15..43bc75d4d 100644
--- a/packages/vitnode/src/components/ui/loader.tsx
+++ b/packages/vitnode/src/components/ui/loader.tsx
@@ -10,7 +10,7 @@ export const Loader = ({
small?: boolean;
}) => {
if (small) {
- return ;
+ return ;
}
return (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 51e751452..a967b1d02 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -321,6 +321,9 @@ importers:
input-otp:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ motion:
+ specifier: ^12.23.6
+ version: 12.23.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)