svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount };
diff --git a/apps/web/src/v2/components/ui/badge.tsx b/apps/web/src/v2/components/ui/badge.tsx
new file mode 100644
index 00000000..6e0a314e
--- /dev/null
+++ b/apps/web/src/v2/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { Slot } from 'radix-ui';
+
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
+ secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
+ destructive:
+ 'bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90',
+ outline:
+ 'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
+ ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 [a&]:hover:underline',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function Badge({
+ className,
+ variant = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'span'> & VariantProps
& { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : 'span';
+
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/apps/web/src/v2/components/ui/breadcrumb.tsx b/apps/web/src/v2/components/ui/breadcrumb.tsx
new file mode 100644
index 00000000..e5e23bba
--- /dev/null
+++ b/apps/web/src/v2/components/ui/breadcrumb.tsx
@@ -0,0 +1,102 @@
+import * as React from 'react';
+import { ChevronRight, MoreHorizontal } from 'lucide-react';
+import { Slot } from 'radix-ui';
+
+import { cn } from '@/lib/utils';
+
+function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
+ return ;
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean;
+}) {
+ const Comp = asChild ? Slot.Root : 'a';
+
+ return (
+
+ );
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
+ return (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ );
+}
+
+function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+
+ More
+
+ );
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/apps/web/src/v2/components/ui/button.tsx b/apps/web/src/v2/components/ui/button.tsx
new file mode 100644
index 00000000..aec5bdc4
--- /dev/null
+++ b/apps/web/src/v2/components/ui/button.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { Slot } from 'radix-ui';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
+ 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
+ 'icon-sm': 'size-8',
+ 'icon-lg': 'size-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function Button({
+ className,
+ variant = 'default',
+ size = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot.Root : 'button';
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/apps/web/src/v2/components/ui/calendar.tsx b/apps/web/src/v2/components/ui/calendar.tsx
new file mode 100644
index 00000000..73b62af3
--- /dev/null
+++ b/apps/web/src/v2/components/ui/calendar.tsx
@@ -0,0 +1,178 @@
+import * as React from 'react';
+import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
+import { DayPicker, getDefaultClassNames, type DayButton } from 'react-day-picker';
+
+import { cn } from '@/lib/utils';
+import { Button, buttonVariants } from '@/v2/components/ui/button';
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = 'label',
+ buttonVariant = 'ghost',
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps['variant'];
+}) {
+ const defaultClassNames = getDefaultClassNames();
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className,
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn('w-fit', defaultClassNames.root),
+ months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
+ month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
+ nav: cn(
+ 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
+ defaultClassNames.nav,
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+ defaultClassNames.button_previous,
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+ defaultClassNames.button_next,
+ ),
+ month_caption: cn(
+ 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
+ defaultClassNames.month_caption,
+ ),
+ dropdowns: cn(
+ 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
+ defaultClassNames.dropdowns,
+ ),
+ dropdown_root: cn(
+ 'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
+ defaultClassNames.dropdown_root,
+ ),
+ dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
+ caption_label: cn(
+ 'font-medium select-none',
+ captionLayout === 'label'
+ ? 'text-sm'
+ : 'flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
+ defaultClassNames.caption_label,
+ ),
+ month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
+ weekdays: cn('flex', defaultClassNames.weekdays),
+ weekday: cn(
+ 'flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none',
+ defaultClassNames.weekday,
+ ),
+ week: cn('mt-2 flex w-full', defaultClassNames.week),
+ week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
+ week_number: cn(
+ 'text-[0.8rem] text-muted-foreground select-none',
+ defaultClassNames.week_number,
+ ),
+ day: cn(
+ 'group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md',
+ props.showWeekNumber
+ ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-md'
+ : '[&:first-child[data-selected=true]_button]:rounded-l-md',
+ defaultClassNames.day,
+ ),
+ range_start: cn('rounded-l-md bg-accent', defaultClassNames.range_start),
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
+ range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
+ today: cn(
+ 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',
+ defaultClassNames.today,
+ ),
+ outside: cn(
+ 'text-muted-foreground aria-selected:text-muted-foreground',
+ defaultClassNames.outside,
+ ),
+ disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
+ hidden: cn('invisible', defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return ;
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === 'left') {
+ return ;
+ }
+
+ if (orientation === 'right') {
+ return ;
+ }
+
+ return ;
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ );
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ );
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames();
+
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus();
+ }, [modifiers.focused]);
+
+ return (
+