diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..76add87
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+dist
\ No newline at end of file
diff --git a/app.tsx b/app.tsx
new file mode 100644
index 0000000..04f6d56
--- /dev/null
+++ b/app.tsx
@@ -0,0 +1,641 @@
+/* eslint-disable max-lines */
+'use client'
+
+import React, { useState } from 'react'
+import { toast } from 'sonner'
+
+import {
+ Button,
+ Checkbox,
+ Combobox,
+ ComboboxContent,
+ ComboboxItem,
+ ComboboxTrigger,
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ Kbd,
+ NavigationMenu,
+ NavigationMenuContent,
+ NavigationMenuItem,
+ NavigationMenuLink,
+ NavigationMenuTrigger,
+ NumberInput,
+ RadioGroup,
+ RadioGroupItem,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ Separator,
+ Skeleton,
+ Switch,
+ Tabs,
+ TabsContent,
+ TabsTrigger,
+ TabsTriggerList,
+ TextArea,
+ TextInput,
+ Toggle,
+ ToggleGroup,
+ ToggleGroupItem,
+} from '@/components'
+import { Field } from '@/components/_shared/components/field/field/field'
+import { FieldLabel } from '@/components/_shared/components/field/field-label/field-label'
+import { FieldMessage } from '@/components/_shared/components/field/field-message/field-message'
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/components/accordion'
+import { AspectRatio } from '@/components/aspect-ratio'
+import { Box } from '@/components/box'
+import {
+ Card,
+ CardBody,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/card'
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/collapsible'
+import { Container } from '@/components/container'
+import { Flex } from '@/components/flex'
+import { Form } from '@/components/form'
+import { Grid } from '@/components/grid'
+import { Heading } from '@/components/heading'
+import { Label } from '@/components/label'
+import { linkClasses } from '@/components/link'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/popover'
+import { Progress } from '@/components/progress'
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@/components/sheet'
+import { Slider, SliderThumb } from '@/components/slider'
+import { Text } from '@/components/text'
+import { Toaster } from '@/components/toast'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/tooltip'
+
+const App = () => {
+ console.log('App')
+
+ const [state, setState] = useState(false)
+
+ return (
+
+
+
+ Press me
+
+ setState(!state)}
+ isLoading={state}
+ variant='solid'
+ color='primary'
+ size='md'
+ loaderProps={{
+ position: 'absolute-center',
+ }}
+ >
+ Press me
+
+
+ Press me
+
+
+ Press me
+
+
+ Press me
+
+
+ Press me
+
+
+ Press me
+
+ setState(!state)}>set loading
+
+
+
+
+ Accordion 1
+ Content 1
+
+
+ Accordion 1
+ Content 1
+
+
+ Accordion 1
+ Content 1
+
+
+
+
+
+ Red
+ Contained
+
+ I am being flexed
+ I am being flexed too
+
+
+
+ I am in a grid
+ I am in a grid too
+ I am in a grid too too
+
+
+
+ Label
+
+
+
+
+ {/* */}
+
+
+ I am header of a card
+ description
+
+ I am a body of a card
+
+ Press me
+ Press me
+
+
+ Hi i am a textual text!
+ Heading 1
+ Heading 1
+ Heading 1
+ Heading 1
+ Heading 1
+ Heading 1
+
+
+
+ Trigger popover
+
+ Content popover Content popoverContent popoverContent popover Content popover
+
+
+
+ Trigger tooltip
+ This is content
+
+
+
+ Sign in
+
+
+ Edit title
+ Editing a title has serious...
+
+ Lovro je najbolji
+
+
+ Cancel
+
+ Save
+
+
+
+
+ Sign up
+
+
+ Edit title
+ Editing a title has serious...
+
+ Lovro je najbolji
+
+ Cancel
+ Save
+
+
+
+
+ Collapsible trigger
+ Content
+
+
+
+
+ Home
+
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+
+
+
+ Business
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+
+
+
+
+ Other
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+ Plisani igracke
+
+ Pronadite najbolje plisane igracke.
+
+
+
+
+
+
+
+
+ Toggle
+
+
+
+ Toggle group
+ Item 1
+ Item 2
+
+
+
+
+
+ Item 1
+ Item 2
+
+ Item 3
+
+
+ Content 1
+ Content 2
+ Content 3
+
+
+
+
+
+ No results.
+
+
+
+
+ option 1
+
+
+
+
+ option 2
+
+
+
+
+ option 3
+
+
+
+
+
+
+
+
+
+
+ Item 1
+ Item 2
+ Item 3
+
+
+
+
+ Select
+
+
+
+ Item 1
+ item 2
+ Item 3
+ Item 4
+ Item 5
+ item 6
+ Item 7
+ Item 8
+ Item 9
+ Item 10
+ item 11
+ Item 12
+ Item 14
+ Item 15
+ item 16
+ Item 17
+ Item 18
+
+
+
+
+ console.log('afe')}
+ keyCombination={['Control', 'k']}
+ keyCombinationOptions={{
+ preventDefault: true,
+ }}
+ >
+ Shift + 3
+
+
+
+ Text input
+
+
+
+ Number input
+
+
+
+ Number input
+
+
+
+
+ Number input
+
+
+
+
+
+ Number input
+
+
+
+
+
+
+
+
+
+ Link
+
+
+
+
+
+
+
+
+
+
+
+ Lablel
+
+
+
+
Content shift
+
+ )
+}
+
+export { App }
diff --git a/globals.css b/globals.css
new file mode 100644
index 0000000..7740478
--- /dev/null
+++ b/globals.css
@@ -0,0 +1,119 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --timing-function: cubic-bezier(0.16, 1, 0.3, 1);
+ --background: 255, 255, 255;
+ --foreground: 10, 10, 10;
+ --mode: 255, 255, 255;
+ --mode-foreground: 10, 10, 10;
+ --mode-contrast: 10, 10, 10;
+ --mode-contrast-foreground: 255, 255, 255;
+ --mode-accent: 16, 16, 235;
+ --mode-accent-foreground: 10, 10, 10;
+ --mode-contrast-accent: 41, 41, 41;
+ --mode-contrast-accent-foreground: 255, 255, 255;
+ --neutral: 64, 64, 191;
+ --neutral-foreground: 10, 10, 10;
+ --primary: 38, 113, 145;
+ --primary-foreground: 250, 250, 250;
+ --secondary: 123, 0, 123;
+ --secondary-foreground: 250, 250, 250;
+ --destructive: 130, 0, 130;
+ --destructive-foreground: 250, 250, 250;
+ --success: 115, 184, 115;
+ --success-foreground: 10, 10, 10;
+ --warning: 140, 140, 0;
+ --warning-foreground: 10, 10, 10;
+ --info: 136, 136, 0;
+ --info-foreground: 10, 10, 10;
+ --help: 166, 0, 166;
+ --help-foreground: 250, 250, 250;
+ --brand: 187, 0, 187;
+ --brand-foreground: 250, 250, 250;
+ --ring-color: 174, 0, 174;
+ --ring-width: 2px;
+ --ring-offset: 2px;
+ --border-width: 2px;
+ --active-pressed-scale: 0.97;
+ --duration-fast: 150ms;
+ --duration-fast-medium: 200ms;
+ --duration-faster-medium: 250ms;
+ --duration-medium: 300ms;
+ --duration-medium-slow: 350ms;
+ --duration-medium-slower: 400ms;
+ --duration-slow: 400ms;
+ --muted: 245, 245, 245;
+ --muted-foreground: 115, 115, 115;
+ --radius: 0.5rem;
+ --border: 228, 228, 228;
+
+ /* components */
+ --card: 254, 254, 254;
+ --skeleton: 16, 16, 235;
+ }
+ .dark {
+ --background: 10, 10, 10;
+ --foreground: 255, 255, 255;
+ --mode: 10, 10, 10;
+ --mode-foreground: 255, 255, 255;
+ --mode-contrast: 255, 255, 255;
+ --mode-contrast-foreground: 10, 10, 10;
+ --mode-accent: 41, 41, 41;
+ --mode-accent-foreground: 255, 255, 255;
+ --mode-contrast-accent: 235, 235, 235;
+ --mode-contrast-accent-foreground: 10, 10, 10;
+ --mode-accent-high: 64, 64, 64;
+ --mode-contrast-accent-high: 191, 191, 191;
+ --neutral: 87, 87, 87;
+ --neutral-foreground: 255, 255, 255;
+ --primary: 0, 112, 240;
+ --primary-foreground: 250, 250, 250;
+ --secondary: 102, 51, 153;
+ --secondary-foreground: 250, 250, 250;
+ --destructive: 243, 18, 96;
+ --destructive-foreground: 250, 250, 250;
+ --success: 34 197 94;
+ --success-foreground: 10, 10, 10;
+ --warning: 255, 165, 0;
+ --warning-foreground: 10, 10, 10;
+ --info: 0, 112, 240;
+ --info-foreground: 250, 250, 250;
+ --help: 128, 0, 128;
+ --help-foreground: 250, 250, 250;
+ --brand: 75, 0, 130;
+ --brand-foreground: 250, 250, 250;
+ --ring-color: 0, 112, 240;
+ --ring-width: 2px;
+ --ring-offset: 2px;
+ --active-pressed-scale: 0.97;
+ --muted: 161, 161, 170;
+ --muted-foreground: 163, 163, 163;
+ --radius: 0.5rem;
+ --screen-xs: 480px;
+ --screen-sm: 640px;
+ --screen-md: 768px;
+ --screen-lg: 1024px;
+ --screen-xl: 1280px;
+ --screen-2xl: 1536px;
+
+ /* Components */
+ --card: 26, 26, 26;
+ --border: 38, 38, 38;
+ --separator: 64, 64, 64;
+ --skeleton: 64, 64, 64;
+ }
+
+ * {
+ font-family: 'Inter', sans-serif;
+ }
+
+ body {
+ background-color: rgb(var(--background));
+ color: rgb(var(--foreground));
+ }
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..69051e2
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Render UI
+
+
+
+
+
+
diff --git a/main.tsx b/main.tsx
new file mode 100644
index 0000000..021cc96
--- /dev/null
+++ b/main.tsx
@@ -0,0 +1,14 @@
+/* eslint-disable no-relative-import-paths/no-relative-import-paths */
+import './globals.css'
+
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+
+import { App } from './app'
+
+// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
+ReactDOM.createRoot(document.querySelector('#root') as HTMLElement).render(
+
+
+ ,
+)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7728391
--- /dev/null
+++ b/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@renderui/core",
+ "version": "0.0.1",
+ "private": false,
+ "description": "React UI library with highly modular and ready-out-of-the-box components",
+ "license": "MIT",
+ "author": "Lovro Žagar",
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/es/index.d.ts",
+ "default": "./dist/es/index.js"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.cts",
+ "default": "./dist/cjs/index.cjs"
+ }
+ }
+ },
+ "main": "./dist/es/index.js",
+ "module": "./dist/es/index.js",
+ "types": "./dist/es/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "bunchee",
+ "dev": "vite dev"
+ },
+ "dependencies": {
+ "@radix-ui/react-collapsible": "^1.0.3",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-navigation-menu": "^1.1.4",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-portal": "^1.0.4",
+ "@radix-ui/react-scroll-area": "^1.0.5",
+ "@radix-ui/react-slider": "^1.1.2",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.0.4",
+ "@radix-ui/react-toggle": "^1.0.3",
+ "@radix-ui/react-toggle-group": "^1.0.4",
+ "@radix-ui/react-tooltip": "^1.0.7",
+ "@renderui/constants": "^0.0.1",
+ "@renderui/hooks": "^0.1.0",
+ "@renderui/types": "^0.0.2",
+ "@renderui/utils": "^0.1.3",
+ "cmdk": "^0.2.1",
+ "react-aria": "^3.32.1",
+ "sonner": "^1.4.3"
+ },
+ "devDependencies": {
+ "autoprefixer": "^10.4.17",
+ "bunchee": "^4.4.6",
+ "framer-motion": "^11.0.5",
+ "postcss": "^8.4.35",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.3.0",
+ "vite": "^5.1.4"
+ },
+ "peerDependencies": {
+ "framer-motion": "^11.0.5",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..393a10f
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,8 @@
+const config = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
+
+export default config
diff --git a/src/components/_shared/classes/input-container-classes.ts b/src/components/_shared/classes/input-container-classes.ts
new file mode 100644
index 0000000..ddc6ccf
--- /dev/null
+++ b/src/components/_shared/classes/input-container-classes.ts
@@ -0,0 +1,18 @@
+import { cva } from '@renderui/utils'
+
+const inputContainerClasses = cva(
+ 'flex min-w-0 cursor-text items-center gap-2 overflow-hidden bg-mode-accent ring-ring-color ring-offset-0 data-[hover=true]:bg-mode-accent/80 data-[focus-within=true]:ring-[2px] data-[hover=true]:data-[focus-within=true]:ring-[2px] data-[hover=true]:ring-[1px]',
+ {
+ variants: {
+ size: {
+ xs: 'h-[32px]',
+ sm: 'h-[36px]',
+ md: 'h-[40px]',
+ lg: 'h-[46px]',
+ xl: 'h-[52px]',
+ },
+ },
+ },
+)
+
+export { inputContainerClasses }
diff --git a/src/components/_shared/components/field/field-context/field-context.ts b/src/components/_shared/components/field/field-context/field-context.ts
new file mode 100644
index 0000000..02053dc
--- /dev/null
+++ b/src/components/_shared/components/field/field-context/field-context.ts
@@ -0,0 +1,16 @@
+import { initializeContext } from '@renderui/utils'
+import React from 'react'
+
+const [FieldProvider, useFieldContext] = initializeContext<{
+ id: string
+ error: React.ReactNode
+}>({
+ errorMessage:
+ 'Components using useField must be wrapped in their a <{InputName}Field /> component.',
+ providerName: 'ToggleGroupProvider',
+ hookName: 'useField',
+ name: 'fieldContext',
+ strict: false,
+})
+
+export { FieldProvider, useFieldContext }
diff --git a/src/components/_shared/components/field/field-label/field-label.tsx b/src/components/_shared/components/field/field-label/field-label.tsx
new file mode 100644
index 0000000..becb075
--- /dev/null
+++ b/src/components/_shared/components/field/field-label/field-label.tsx
@@ -0,0 +1,26 @@
+'use client'
+
+import { cx } from 'class-variance-authority'
+import React from 'react'
+
+import { useFieldContext } from '@/components/_shared/components/field/field-context/field-context'
+import { Label } from '@/components/label'
+
+const FieldLabel = (props) => {
+ const { children, info, className, ...restProps } = props
+
+ const { id } = useFieldContext()
+
+ return (
+
+ {children}
+ {info}
+
+ )
+}
+
+export { FieldLabel }
diff --git a/src/components/_shared/components/field/field-message/field-message.tsx b/src/components/_shared/components/field/field-message/field-message.tsx
new file mode 100644
index 0000000..6048678
--- /dev/null
+++ b/src/components/_shared/components/field/field-message/field-message.tsx
@@ -0,0 +1,32 @@
+'use client'
+
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { useFieldContext } from '@/components/_shared/components/field/field-context/field-context'
+
+const FieldMessage = (props) => {
+ const { children } = props
+
+ const { error } = useFieldContext()
+
+ const hasDescription = children !== undefined
+ const hasError = error !== undefined
+
+ return (
+
+ {error ?? children}
+
+ )
+}
+
+export { FieldMessage }
diff --git a/src/components/_shared/components/field/field/field.tsx b/src/components/_shared/components/field/field/field.tsx
new file mode 100644
index 0000000..b915529
--- /dev/null
+++ b/src/components/_shared/components/field/field/field.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+
+import { FieldProvider } from '@/components/_shared/components/field/field-context/field-context'
+
+const Field = React.forwardRef((props, ref) => {
+ const { children, error } = props
+
+ const id = React.useId()
+
+ return (
+
+ {children}
+
+ )
+})
+
+Field.displayName = 'Field'
+
+export { Field }
diff --git a/src/components/_shared/components/modal-close/modal-close.tsx b/src/components/_shared/components/modal-close/modal-close.tsx
new file mode 100644
index 0000000..db97990
--- /dev/null
+++ b/src/components/_shared/components/modal-close/modal-close.tsx
@@ -0,0 +1,25 @@
+import { DialogClose as DialogClosePrimitive } from '@radix-ui/react-dialog'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+type DialogClosePrimitiveType = typeof DialogClosePrimitive
+
+type DialogCloseRef = React.ElementRef
+
+type DialogCloseProps = React.ComponentProps
+
+const ModalClose = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+ModalClose.displayName = 'ModalClose'
+
+export { ModalClose }
diff --git a/src/components/_shared/components/modal-description/modal-description.tsx b/src/components/_shared/components/modal-description/modal-description.tsx
new file mode 100644
index 0000000..8541edc
--- /dev/null
+++ b/src/components/_shared/components/modal-description/modal-description.tsx
@@ -0,0 +1,22 @@
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Text, TextProps, TextRef } from '@/components/text'
+
+const ModalDescription = React.forwardRef((props, ref) => {
+ const { className, size = 'sm', ...restProps } = props
+
+ return (
+
+ )
+})
+
+ModalDescription.displayName = 'ModalDescription'
+
+export { ModalDescription }
diff --git a/src/components/_shared/components/modal-footer/modal-footer.tsx b/src/components/_shared/components/modal-footer/modal-footer.tsx
new file mode 100644
index 0000000..4bff39f
--- /dev/null
+++ b/src/components/_shared/components/modal-footer/modal-footer.tsx
@@ -0,0 +1,28 @@
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Flex, FlexProps, FlexRef } from '@/components/flex'
+
+type ModalFooterRef = FlexRef
+
+type ModalFooterProps = FlexProps
+
+const ModalFooter = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+ModalFooter.displayName = 'ModalFooter'
+
+export { ModalFooter }
diff --git a/src/components/_shared/components/modal-header/modal-header.tsx b/src/components/_shared/components/modal-header/modal-header.tsx
new file mode 100644
index 0000000..681f505
--- /dev/null
+++ b/src/components/_shared/components/modal-header/modal-header.tsx
@@ -0,0 +1,21 @@
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Grid, GridProps, GridRef } from '@/components/grid'
+
+const ModalHeader = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+ModalHeader.displayName = 'ModalHeader'
+
+export { ModalHeader }
diff --git a/src/components/_shared/components/modal-title/modal-title.tsx b/src/components/_shared/components/modal-title/modal-title.tsx
new file mode 100644
index 0000000..6422273
--- /dev/null
+++ b/src/components/_shared/components/modal-title/modal-title.tsx
@@ -0,0 +1,27 @@
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Text, TextProps, TextRef } from '@/components/text'
+
+type ModalTitleRef = TextRef
+
+type ModalTitleProps = TextProps
+
+const ModalTitle = React.forwardRef((props, ref) => {
+ const { className, size = 'lg', color = 'mode-contrast', ...restProps } = props
+
+ return (
+
+ )
+})
+
+ModalTitle.displayName = 'ModalTitle'
+
+export { ModalTitle }
diff --git a/src/components/_shared/hooks/use-aria-handlers.ts b/src/components/_shared/hooks/use-aria-handlers.ts
new file mode 100644
index 0000000..8e4912c
--- /dev/null
+++ b/src/components/_shared/hooks/use-aria-handlers.ts
@@ -0,0 +1,196 @@
+import { OrUndefined } from '@renderui/types'
+import { mergeProps } from '@renderui/utils'
+import React from 'react'
+import {
+ chain,
+ FocusProps,
+ FocusRingProps,
+ HoverProps,
+ LongPressProps,
+ PressEvent,
+ PressHookProps,
+ useFocus,
+ useFocusRing,
+ useHover,
+ useLongPress,
+ usePress,
+} from 'react-aria'
+
+import { UseAriaHandlersProps } from '@/components/_shared/types/aria'
+import { isKeyboardPointerType } from '@/components/_shared/utils/is-keyboard-pointer-type'
+
+function useAriaHandlers(
+ props: OrUndefined,
+ ref: React.ForwardedRef | undefined,
+) {
+ const {
+ // PRESS
+ isPressDisabled,
+ isPressed: isPressedControlled,
+ preventFocusOnPress,
+ allowTextSelectionOnPress,
+ shouldCancelOnPointerExit,
+ onPress,
+ onPressStart,
+ onPressEnd,
+ onPressChange,
+ onPressUp,
+
+ // KEYBOARD PRESS
+ onKeyboardPressStart,
+ onKeyboardPressEnd,
+ onKeyboardPress,
+
+ // LONG PRESS
+ isLongPressDisabled,
+ longPressTreshold,
+ longPressAccessibilityDescription,
+ onLongPressStart,
+ onLongPress,
+ onLongPressEnd,
+
+ // FOCUS
+ isFocusDisabled,
+ onFocus,
+ onFocusChange,
+ onBlur,
+
+ // FOCUS WITHIN
+ isTextInput,
+ isFocusWithin,
+
+ // HOVER
+ isHoverDisabled,
+ onHoverStart,
+ onHoverChange,
+ onHoverEnd,
+
+ // NATIVE
+ onDragStart,
+ onKeyDown,
+ onKeyUp,
+ onMouseDown,
+ onPointerDown,
+ onPointerEnter,
+ onPointerLeave,
+ onPointerUp,
+
+ isUsingAriaPressProps = true,
+ } = props
+
+ const [isKeyboardPressed, setIsKeyboardPressed] = React.useState(false)
+ const [isLongPressed, setIsLongPressed] = React.useState(false)
+
+ const handlePressStart = (event: PressEvent) => {
+ if (isKeyboardPointerType(event)) setIsKeyboardPressed(true)
+ }
+
+ const handlePressEnd = (event: PressEvent) => {
+ if (isKeyboardPointerType(event)) setIsKeyboardPressed(false)
+ }
+
+ const handleKeyboardPressStart = (event: PressEvent) => {
+ if (isKeyboardPointerType(event) && onKeyboardPressStart) onKeyboardPressStart(event)
+ }
+
+ const handleKeyboardPressEnd = (event: PressEvent) => {
+ if (isKeyboardPointerType(event) && onKeyboardPressEnd) onKeyboardPressEnd(event)
+ }
+
+ const handleKeyboardPress = (event: PressEvent) => {
+ if (isKeyboardPointerType(event) && onKeyboardPress) onKeyboardPress(event)
+ }
+
+ // hooks propeties are asserted due to the decision to keep exactOptionalPropertyTypes: true - TS rule
+
+ const { pressProps, isPressed } = usePress({
+ ref,
+ isPressed: isPressedControlled,
+ preventFocusOnPress,
+ allowTextSelectionOnPress,
+ shouldCancelOnPointerExit,
+ isDisabled: isPressDisabled,
+ onPressChange,
+ onPressUp,
+ onPress: chain(handleKeyboardPress, onPress),
+ onPressStart: chain(handlePressStart, handleKeyboardPressStart, onPressStart),
+ onPressEnd: chain(handlePressEnd, handleKeyboardPressEnd, onPressEnd),
+ } as PressHookProps)
+
+ const { longPressProps } = useLongPress({
+ isDisabled: isLongPressDisabled,
+ threshold: longPressTreshold,
+ accessibilityDescription: longPressAccessibilityDescription,
+ onLongPressStart: chain(onLongPressStart, () => setIsLongPressed(true)),
+ onLongPress,
+ onLongPressEnd,
+ } as LongPressProps)
+
+ const { focusProps } = useFocus({
+ isDisabled: isFocusDisabled,
+ onFocus,
+ onBlur,
+ onFocusChange,
+ } as FocusProps)
+
+ const {
+ focusProps: focusVisibleProps,
+ isFocusVisible,
+ isFocused,
+ } = useFocusRing({
+ isTextInput,
+ within: isFocusWithin,
+ } as FocusRingProps)
+
+ const { hoverProps, isHovered } = useHover({
+ isDisabled: isHoverDisabled,
+ onHoverStart,
+ onHoverChange,
+ onHoverEnd,
+ } as HoverProps)
+
+ const ariaHandlerProps = mergeProps(
+ isLongPressDisabled ? undefined : longPressProps,
+ isUsingAriaPressProps ? pressProps : undefined,
+ focusProps,
+ focusVisibleProps,
+ hoverProps,
+ {
+ onPointerUp: chain(onPointerUp, () => setIsLongPressed(false)),
+ onPointerLeave: chain(onPointerLeave, () => setIsLongPressed(false)),
+ onDragStart,
+ onKeyDown,
+ onKeyUp,
+ onMouseDown,
+ onPointerDown,
+ onPointerEnter,
+ },
+ )
+
+ const accessibilityProps = {
+ 'aria-pressed': isPressed,
+ 'data-pressed': isPressed,
+ 'data-long-pressed': isLongPressed,
+ 'data-keyboard-pressed': isKeyboardPressed,
+ 'data-hover': isHovered,
+ 'data-focus-within': isFocusWithin ? isFocused : undefined,
+ 'data-focus': isFocusWithin ? undefined : isFocused,
+ 'data-focus-visible': isFocusVisible,
+ }
+
+ return {
+ ariaComponentProps: {
+ ...accessibilityProps,
+ ...ariaHandlerProps,
+ },
+ ariaFlags: {
+ isPressed,
+ isKeyboardPressed,
+ isFocused,
+ isFocusVisible,
+ isHovered,
+ },
+ }
+}
+
+export { useAriaHandlers }
diff --git a/src/components/_shared/types/aria.ts b/src/components/_shared/types/aria.ts
new file mode 100644
index 0000000..50a53d1
--- /dev/null
+++ b/src/components/_shared/types/aria.ts
@@ -0,0 +1,53 @@
+import {
+ AriaFocusRingProps,
+ FocusProps,
+ HoverProps,
+ LongPressProps,
+ PressEvent,
+ PressProps,
+} from 'react-aria'
+
+type UseAriaHandlersProps = {
+ isPressDisabled: PressProps['isDisabled'] | undefined
+ isPressed: PressProps['isPressed'] | undefined
+ preventFocusOnPress: PressProps['preventFocusOnPress'] | undefined
+ allowTextSelectionOnPress: PressProps['allowTextSelectionOnPress'] | undefined
+ shouldCancelOnPointerExit: PressProps['shouldCancelOnPointerExit'] | undefined
+ onPress: PressProps['onPress'] | undefined
+ onPressStart: PressProps['onPressStart'] | undefined
+ onPressEnd: PressProps['onPressEnd'] | undefined
+ onPressChange: PressProps['onPressChange'] | undefined
+ onPressUp: PressProps['onPressUp'] | undefined
+ onKeyboardPress: ((event: PressEvent) => void) | undefined
+ onKeyboardPressStart: ((event: PressEvent) => void) | undefined
+ onKeyboardPressEnd: ((event: PressEvent) => void) | undefined
+ isLongPressDisabled: LongPressProps['isDisabled'] | undefined
+ longPressTreshold: LongPressProps['threshold'] | undefined
+ longPressAccessibilityDescription: LongPressProps['accessibilityDescription'] | undefined
+ onLongPressStart: LongPressProps['onLongPressStart'] | undefined
+ onLongPress: LongPressProps['onLongPress'] | undefined
+ onLongPressEnd: LongPressProps['onLongPressEnd'] | undefined
+ isFocusDisabled: FocusProps['isDisabled'] | undefined
+ onFocus: FocusProps['onFocus'] | undefined
+ onFocusChange: FocusProps['onFocusChange'] | undefined
+ onBlur: FocusProps['onBlur'] | undefined
+ isTextInput: AriaFocusRingProps['isTextInput'] | undefined
+ isFocusWithin: AriaFocusRingProps['within'] | undefined
+ isHoverDisabled: HoverProps['isDisabled'] | undefined
+ onHoverStart: HoverProps['onHoverStart'] | undefined
+ onHoverChange: HoverProps['onHoverChange'] | undefined
+ onHoverEnd: HoverProps['onHoverEnd'] | undefined
+ isUsingAriaPressProps?: boolean
+ onDragStart: React.DragEventHandler | undefined
+ onKeyDown: React.KeyboardEventHandler | undefined
+ onKeyUp: React.KeyboardEventHandler | undefined
+ onMouseDown: React.MouseEventHandler | undefined
+ onPointerDown: React.PointerEventHandler | undefined
+ onPointerEnter: React.PointerEventHandler | undefined
+ onPointerLeave: React.PointerEventHandler | undefined
+ onPointerUp: React.PointerEventHandler | undefined
+}
+
+type OptionalAriaProps = Partial
+
+export type { OptionalAriaProps, UseAriaHandlersProps }
diff --git a/src/components/_shared/types/as-child.ts b/src/components/_shared/types/as-child.ts
new file mode 100644
index 0000000..aaaee6a
--- /dev/null
+++ b/src/components/_shared/types/as-child.ts
@@ -0,0 +1,5 @@
+type AsChildProp = {
+ asChild?: boolean
+}
+
+export { AsChildProp }
diff --git a/src/components/_shared/types/colors.ts b/src/components/_shared/types/colors.ts
new file mode 100644
index 0000000..9c22fec
--- /dev/null
+++ b/src/components/_shared/types/colors.ts
@@ -0,0 +1,13 @@
+type Color =
+ | 'mode'
+ | 'mode-accent'
+ | 'mode-contrast'
+ | 'mode-contrast-accent'
+ | 'primary'
+ | 'secondary'
+ | 'destructive'
+ | 'success'
+ | 'warning'
+ | 'info'
+
+export type { Color }
diff --git a/src/components/_shared/types/variants.ts b/src/components/_shared/types/variants.ts
new file mode 100644
index 0000000..867409b
--- /dev/null
+++ b/src/components/_shared/types/variants.ts
@@ -0,0 +1,7 @@
+import { VariantProps } from '@renderui/utils'
+
+type NonNullableVariantProps any> = {
+ [K in keyof VariantProps]: NonNullable[K]>
+}
+
+export type { NonNullableVariantProps }
diff --git a/src/components/_shared/utils/focus-input.ts b/src/components/_shared/utils/focus-input.ts
new file mode 100644
index 0000000..e540009
--- /dev/null
+++ b/src/components/_shared/utils/focus-input.ts
@@ -0,0 +1,15 @@
+import React from 'react'
+
+function focusInput(inputRef: React.RefObject) {
+ if (!inputRef?.current) return
+
+ const { current: input } = inputRef
+
+ input.focus()
+
+ const valueLength = input.value.length
+
+ input.setSelectionRange(valueLength, valueLength)
+}
+
+export { focusInput }
diff --git a/src/components/_shared/utils/is-keyboard-pointer-type.ts b/src/components/_shared/utils/is-keyboard-pointer-type.ts
new file mode 100644
index 0000000..939aa46
--- /dev/null
+++ b/src/components/_shared/utils/is-keyboard-pointer-type.ts
@@ -0,0 +1,7 @@
+import { PressEvent } from 'react-aria'
+
+function isKeyboardPointerType(event: PressEvent) {
+ return event.pointerType === 'keyboard'
+}
+
+export { isKeyboardPointerType }
diff --git a/src/components/_shared/utils/split-aria-props.ts b/src/components/_shared/utils/split-aria-props.ts
new file mode 100644
index 0000000..ec8213f
--- /dev/null
+++ b/src/components/_shared/utils/split-aria-props.ts
@@ -0,0 +1,93 @@
+import { UseAriaHandlersProps } from '@/components/_shared/types/aria'
+
+type AriaHandlerPropsSelectorProps = T & Partial
+
+const splitAriaProps = (props: AriaHandlerPropsSelectorProps) => {
+ const {
+ isPressDisabled,
+ isPressed,
+ preventFocusOnPress,
+ allowTextSelectionOnPress,
+ shouldCancelOnPointerExit,
+ onPress,
+ onPressStart,
+ onPressEnd,
+ onPressChange,
+ onPressUp,
+ onKeyboardPress,
+ onKeyboardPressStart,
+ onKeyboardPressEnd,
+ isLongPressDisabled = true,
+ longPressTreshold,
+ longPressAccessibilityDescription,
+ onLongPressStart,
+ onLongPress,
+ onLongPressEnd,
+ isFocusDisabled,
+ onFocus,
+ onFocusChange,
+ onBlur,
+ isTextInput,
+ isFocusWithin,
+ isHoverDisabled,
+ onHoverStart,
+ onHoverChange,
+ onHoverEnd,
+ onDragStart,
+ onKeyDown,
+ onKeyUp,
+ onMouseDown,
+ onPointerDown,
+ onPointerEnter,
+ onPointerLeave,
+ onPointerUp,
+ ...nonAriaProps
+ } = props
+
+ const ariaProps = {
+ isPressDisabled,
+ isPressed,
+ preventFocusOnPress,
+ allowTextSelectionOnPress,
+ shouldCancelOnPointerExit,
+ onPress,
+ onPressStart,
+ onPressEnd,
+ onPressChange,
+ onPressUp,
+ onKeyboardPress,
+ onKeyboardPressStart,
+ onKeyboardPressEnd,
+ isLongPressDisabled,
+ longPressTreshold,
+ longPressAccessibilityDescription,
+ onLongPressStart,
+ onLongPress,
+ onLongPressEnd,
+ isFocusDisabled,
+ onFocus,
+ onFocusChange,
+ onBlur,
+ isTextInput,
+ isFocusWithin,
+ isHoverDisabled,
+ onHoverStart,
+ onHoverChange,
+ onHoverEnd,
+ onDragStart,
+ onKeyDown,
+ onKeyUp,
+ onMouseDown,
+ onPointerDown,
+ onPointerEnter,
+ onPointerLeave,
+ onPointerUp,
+ }
+
+ return {
+ ariaProps,
+ nonAriaProps,
+ }
+}
+
+export { splitAriaProps }
diff --git a/src/components/_shared/variants/letter-spacing.ts b/src/components/_shared/variants/letter-spacing.ts
new file mode 100644
index 0000000..316212b
--- /dev/null
+++ b/src/components/_shared/variants/letter-spacing.ts
@@ -0,0 +1,11 @@
+const letterSpacingVariants = {
+ tightest: ['tracking-[-0.1em]'],
+ tighter: ['tracking-tighter'],
+ tight: ['tracking-tight'],
+ base: ['tracking-normal'],
+ wide: ['tracking-wide'],
+ wider: ['tracking-wider'],
+ widest: ['tracking-widest'],
+} as const
+
+export { letterSpacingVariants }
diff --git a/src/components/_shared/variants/text-break.ts b/src/components/_shared/variants/text-break.ts
new file mode 100644
index 0000000..130d501
--- /dev/null
+++ b/src/components/_shared/variants/text-break.ts
@@ -0,0 +1,8 @@
+const textBreakVariants = {
+ none: 'keep-all [overflow-wrap:normal]',
+ words: 'break-words [overflow-wrap:break-word]',
+ normal: 'break-normal [overflow-wrap:normal]',
+ all: 'break-all [overflow-wrap:anywhere]',
+} as const
+
+export { textBreakVariants }
diff --git a/src/components/_shared/variants/text-overflow.ts b/src/components/_shared/variants/text-overflow.ts
new file mode 100644
index 0000000..3590bb1
--- /dev/null
+++ b/src/components/_shared/variants/text-overflow.ts
@@ -0,0 +1,6 @@
+const textOverflowVariants = {
+ elipsis: ['truncate'],
+ clip: ['overflow-hidden', 'whitespace-nowrap', 'text-clip'],
+} as const
+
+export { textOverflowVariants }
diff --git a/src/components/_shared/variants/text-shadow.ts b/src/components/_shared/variants/text-shadow.ts
new file mode 100644
index 0000000..21a0625
--- /dev/null
+++ b/src/components/_shared/variants/text-shadow.ts
@@ -0,0 +1,9 @@
+const textShadowVariants = {
+ xs: ['[&]:text-shadow-xs', 'shadow-current'],
+ sm: ['[&]:text-shadow-sm', 'shadow-current'],
+ md: ['[&]:text-shadow', 'shadow-current'],
+ lg: ['[&]:text-shadow-lg', 'shadow-current'],
+ xl: ['[&]:text-shadow-xs', 'shadow-current'],
+} as const
+
+export { textShadowVariants }
diff --git a/src/components/_shared/variants/text-size.ts b/src/components/_shared/variants/text-size.ts
new file mode 100644
index 0000000..abf7293
--- /dev/null
+++ b/src/components/_shared/variants/text-size.ts
@@ -0,0 +1,18 @@
+const textSizeVariants = {
+ 'xs': ['text-xs'],
+ 'sm': ['text-sm'],
+ 'base': ['text-base'],
+ 'md': ['text-[1.0675rem]', 'leading-[1.5675rem]'],
+ 'lg': ['text-lg'],
+ 'xl': ['text-xl'],
+ '2xl': ['text-2xl'],
+ '3xl': ['text-3xl'],
+ '4xl': ['text-4xl'],
+ '5xl': ['text-5xl'],
+ '6xl': ['text-6xl'],
+ '7xl': ['text-7xl'],
+ '8xl': ['text-8xl'],
+ '9xl': ['text-9xl'],
+} as const
+
+export { textSizeVariants }
diff --git a/src/components/accordion/components/accordion-content.tsx b/src/components/accordion/components/accordion-content.tsx
new file mode 100644
index 0000000..f5b472c
--- /dev/null
+++ b/src/components/accordion/components/accordion-content.tsx
@@ -0,0 +1,48 @@
+import { AccordionContent as AccordionContentPrimitive } from '@radix-ui/react-accordion'
+import { cn, getOptionalObject, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_ACCORDION_CONTENT_CHILDREN_CONTAINER_CLASSNAME,
+ DEFAULT_ACCORDION_CONTENT_CLASSNAME,
+} from '@/components/accordion/constants/constants'
+import {
+ AccordionContentProps,
+ AccordionContentRef,
+} from '@/components/accordion/types/accordion-content'
+
+const AccordionContent = React.forwardRef(
+ (props, ref) => {
+ const { className, children, childrenContainerProps, ...restProps } = props
+
+ const {
+ asChild,
+ className: childrenContainerClassName,
+ ...restChildrenContainerProps
+ } = getOptionalObject(childrenContainerProps)
+
+ const AccordionContentChildrenContainer = polymorphic(asChild, 'div')
+
+ return (
+
+
+ {children}
+
+
+ )
+ },
+)
+
+AccordionContent.displayName = 'AccordionContent'
+
+export { AccordionContent }
diff --git a/src/components/accordion/components/accordion-item.tsx b/src/components/accordion/components/accordion-item.tsx
new file mode 100644
index 0000000..2920cc4
--- /dev/null
+++ b/src/components/accordion/components/accordion-item.tsx
@@ -0,0 +1,22 @@
+import { AccordionItem as AccordionItemPrimitive } from '@radix-ui/react-accordion'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_ACCORDION_ITEM_CLASSNAME } from '@/components/accordion/constants/constants'
+import { AccordionItemProps, AccordionItemRef } from '@/components/accordion/types/accordion-item'
+
+const AccordionItem = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+AccordionItem.displayName = 'AccordionItem'
+
+export { AccordionItem }
diff --git a/src/components/accordion/components/accordion-trigger.tsx b/src/components/accordion/components/accordion-trigger.tsx
new file mode 100644
index 0000000..08b806b
--- /dev/null
+++ b/src/components/accordion/components/accordion-trigger.tsx
@@ -0,0 +1,77 @@
+import {
+ AccordionHeader as AccordionHeaderPrimitive,
+ AccordionTrigger as AccordionTriggerPrimitive,
+} from '@radix-ui/react-accordion'
+import { ChevronDownIcon } from '@radix-ui/react-icons'
+import { cn, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_ACCORDION_HEADER_CLASSNAME,
+ DEFAULT_ACCORDION_TRIGGER_CLASSNAME,
+ DEFAULT_ACCORDION_TRIGGER_ICON_CLASSNAME,
+} from '@/components/accordion/constants/constants'
+import {
+ AccordionTriggerProps,
+ AccordionTriggerRef,
+} from '@/components/accordion/types/accordion-trigger'
+import { Button } from '@/components/button'
+
+const AccordionTrigger = React.forwardRef(
+ (props, ref) => {
+ const {
+ className,
+ children,
+ icon,
+ iconProps,
+ accordionHeaderProps,
+ hasIcon = true,
+ hasRipple = false,
+ hasDefaultPressedStyles = false,
+ ...restProps
+ } = props
+
+ const { className: headerClassName, ...restAccordionHeaderProps } =
+ getOptionalObject(accordionHeaderProps)
+
+ const { className: iconClassName, ...restIconProps } = getOptionalObject(iconProps)
+
+ const renderIcon = () => {
+ if (!hasIcon) return null
+
+ if (icon) return icon
+
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+ {children}
+ {renderIcon()}
+
+
+
+ )
+ },
+)
+
+AccordionTrigger.displayName = 'AccordionTrigger'
+
+export { AccordionTrigger }
diff --git a/src/components/accordion/components/accordion.tsx b/src/components/accordion/components/accordion.tsx
new file mode 100644
index 0000000..70a7713
--- /dev/null
+++ b/src/components/accordion/components/accordion.tsx
@@ -0,0 +1,19 @@
+'use client'
+
+import { Accordion as AccordionPrimitive } from '@radix-ui/react-accordion'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { AccordionProps, AccordionRef } from '@/components/accordion/types/accordion'
+
+const Accordion = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+Accordion.displayName = 'Accordion'
+
+export { Accordion }
diff --git a/src/components/accordion/constants/constants.ts b/src/components/accordion/constants/constants.ts
new file mode 100644
index 0000000..69c98b3
--- /dev/null
+++ b/src/components/accordion/constants/constants.ts
@@ -0,0 +1,25 @@
+const DEFAULT_ACCORDION_ITEM_CLASSNAME =
+ 'render-ui-accordion-item border-b border-mode-accent transition-[border] duration-fast'
+
+const DEFAULT_ACCORDION_HEADER_CLASSNAME = 'render-ui-accordion-header flex'
+
+const DEFAULT_ACCORDION_TRIGGER_CLASSNAME =
+ 'render-ui-accordion-trigger flex flex-1 rounded-none items-center justify-between px-0 py-4 data-[focus-visible=true]:ring-offset-0 text-sm data-[hover=true]:underline [&[data-state=open]>svg]:rotate-180'
+
+const DEFAULT_ACCORDION_TRIGGER_ICON_CLASSNAME =
+ 'render-ui-accordion-trigger-icon h-4 w-4 shrink-0 text-muted-foreground transition-[color,transform] duration-fast text-mode-contrast'
+
+const DEFAULT_ACCORDION_CONTENT_CLASSNAME =
+ 'render-ui-accordion-content overflow-hidden text-sm text-mode-contrast data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down'
+
+const DEFAULT_ACCORDION_CONTENT_CHILDREN_CONTAINER_CLASSNAME =
+ 'render-ui-accordion-content-children-container pb-4 pt-0'
+
+export {
+ DEFAULT_ACCORDION_CONTENT_CHILDREN_CONTAINER_CLASSNAME,
+ DEFAULT_ACCORDION_CONTENT_CLASSNAME,
+ DEFAULT_ACCORDION_HEADER_CLASSNAME,
+ DEFAULT_ACCORDION_ITEM_CLASSNAME,
+ DEFAULT_ACCORDION_TRIGGER_CLASSNAME,
+ DEFAULT_ACCORDION_TRIGGER_ICON_CLASSNAME,
+}
diff --git a/src/components/accordion/index.ts b/src/components/accordion/index.ts
new file mode 100644
index 0000000..90dc98a
--- /dev/null
+++ b/src/components/accordion/index.ts
@@ -0,0 +1,8 @@
+export * from '@/components/accordion/components/accordion'
+export * from '@/components/accordion/components/accordion-content'
+export * from '@/components/accordion/components/accordion-item'
+export * from '@/components/accordion/components/accordion-trigger'
+export * from '@/components/accordion/types/accordion'
+export * from '@/components/accordion/types/accordion-content'
+export * from '@/components/accordion/types/accordion-item'
+export * from '@/components/accordion/types/accordion-trigger'
diff --git a/src/components/accordion/types/accordion-content.ts b/src/components/accordion/types/accordion-content.ts
new file mode 100644
index 0000000..eaf0844
--- /dev/null
+++ b/src/components/accordion/types/accordion-content.ts
@@ -0,0 +1,21 @@
+import { AccordionContent as AccordionContentPrimitive } from '@radix-ui/react-accordion'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type AccordionContentPrimitiveType = typeof AccordionContentPrimitive
+
+type AccordionContentRef = React.ElementRef
+
+type AccordionContentPrimitiveProps = React.ComponentPropsWithoutRef
+
+type AccordionContentChildrenContainerProps = {
+ childrenContainerProps?: Simplify & AsChildProp>
+}
+
+type AccordionContentProps = Simplify<
+ AccordionContentPrimitiveProps & AccordionContentChildrenContainerProps
+>
+
+export type { AccordionContentProps, AccordionContentRef }
diff --git a/src/components/accordion/types/accordion-item.ts b/src/components/accordion/types/accordion-item.ts
new file mode 100644
index 0000000..4056e08
--- /dev/null
+++ b/src/components/accordion/types/accordion-item.ts
@@ -0,0 +1,10 @@
+import { AccordionItem as AccordionItemPrimitive } from '@radix-ui/react-accordion'
+import React from 'react'
+
+type AccordionItemPrimitiveType = typeof AccordionItemPrimitive
+
+type AccordionItemRef = React.ElementRef
+
+type AccordionItemProps = React.ComponentPropsWithoutRef
+
+export type { AccordionItemProps, AccordionItemRef }
diff --git a/src/components/accordion/types/accordion-trigger.ts b/src/components/accordion/types/accordion-trigger.ts
new file mode 100644
index 0000000..2487665
--- /dev/null
+++ b/src/components/accordion/types/accordion-trigger.ts
@@ -0,0 +1,23 @@
+import { AccordionHeader as AccordionHeaderPrimitive } from '@radix-ui/react-accordion'
+import { ChevronDownIcon } from '@radix-ui/react-icons'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { ButtonProps, ButtonRef } from '@/components/button'
+
+
+type AccordionTriggerRef = ButtonRef
+
+type AccordionTriggerButtonProps = Omit
+
+type AccordionTriggerTriggerProps = {
+ children?: React.ReactNode
+ hasIcon?: boolean
+ icon?: React.ReactNode
+ iconProps?: React.ComponentPropsWithoutRef
+ accordionHeaderProps?: React.ComponentPropsWithoutRef
+}
+
+type AccordionTriggerProps = Simplify
+
+export type { AccordionTriggerProps, AccordionTriggerRef }
diff --git a/src/components/accordion/types/accordion.ts b/src/components/accordion/types/accordion.ts
new file mode 100644
index 0000000..1136004
--- /dev/null
+++ b/src/components/accordion/types/accordion.ts
@@ -0,0 +1,9 @@
+import { Accordion as AccordionPrimitive } from '@radix-ui/react-accordion'
+
+type AccordionPrimitiveType = typeof AccordionPrimitive
+
+type AccordionRef = React.ElementRef
+
+type AccordionProps = React.ComponentPropsWithoutRef
+
+export type { AccordionProps, AccordionRef }
diff --git a/src/components/aria/components/aria.tsx b/src/components/aria/components/aria.tsx
new file mode 100644
index 0000000..a305f63
--- /dev/null
+++ b/src/components/aria/components/aria.tsx
@@ -0,0 +1,61 @@
+'use client'
+
+import { useMergedRef } from '@renderui/hooks'
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { useAriaHandlers } from '@/components/_shared/hooks/use-aria-handlers'
+import { splitAriaProps } from '@/components/_shared/utils/split-aria-props'
+import { DEFAULT_ARIA_CLASSNAME } from '@/components/aria/constants/constants'
+import { AriaProps, AriaRef } from '@/components/aria/types/aria'
+
+const Aria = React.forwardRef((props, ref) => {
+ const { ariaProps, nonAriaProps } = splitAriaProps(props)
+
+ const internalRef = React.useRef(null)
+ const mergedRefCallback = useMergedRef([internalRef, ref])
+
+ const {
+ isPressDisabled,
+ isFocusDisabled,
+ isLongPressDisabled,
+ isHoverDisabled,
+ ...restAriaProps
+ } = ariaProps
+
+ const {
+ className,
+ isDisabled,
+ asChild,
+ isUsingAriaPressProps = false,
+ ...restNonAriaProps
+ } = nonAriaProps
+
+ const { ariaComponentProps } = useAriaHandlers(
+ {
+ ...restAriaProps,
+ isPressDisabled: isDisabled || isPressDisabled,
+ isFocusDisabled: isDisabled || isFocusDisabled,
+ isLongPressDisabled: isDisabled || isLongPressDisabled,
+ isHoverDisabled: isDisabled || isHoverDisabled,
+ isUsingAriaPressProps,
+ },
+ internalRef,
+ )
+
+ const Component = polymorphic(asChild, 'div')
+
+ return (
+
+ )
+})
+
+Aria.displayName = 'Aria'
+
+export { Aria }
diff --git a/src/components/aria/constants/constants.ts b/src/components/aria/constants/constants.ts
new file mode 100644
index 0000000..707d997
--- /dev/null
+++ b/src/components/aria/constants/constants.ts
@@ -0,0 +1,3 @@
+const DEFAULT_ARIA_CLASSNAME = 'render-ui-aria data-[disabled=true]:cursor-not-allowed'
+
+export { DEFAULT_ARIA_CLASSNAME }
diff --git a/src/components/aria/index.ts b/src/components/aria/index.ts
new file mode 100644
index 0000000..161c654
--- /dev/null
+++ b/src/components/aria/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/aria/components/aria'
+export * from '@/components/aria/types/aria'
diff --git a/src/components/aria/types/aria.ts b/src/components/aria/types/aria.ts
new file mode 100644
index 0000000..ef46310
--- /dev/null
+++ b/src/components/aria/types/aria.ts
@@ -0,0 +1,18 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { OptionalAriaProps } from '@/components/_shared/types/aria'
+
+type AriaRef = HTMLElement
+
+type AriaPrimitiveProps = React.ComponentPropsWithoutRef<'div'>
+
+type AriaRenderUIProps = OptionalAriaProps & {
+ asChild?: boolean
+ isDisabled?: boolean
+ isUsingAriaPressProps?: boolean
+}
+
+type AriaProps = Simplify
+
+export type { AriaProps, AriaRef }
diff --git a/src/components/aspect-ratio/components/aspect-ratio.tsx b/src/components/aspect-ratio/components/aspect-ratio.tsx
new file mode 100644
index 0000000..073ba82
--- /dev/null
+++ b/src/components/aspect-ratio/components/aspect-ratio.tsx
@@ -0,0 +1,19 @@
+import { polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { AspectRatioProps, AspectRatioRef } from '@/components/aspect-ratio/types/aspect-ratio'
+import { getMergedStyles } from '@/components/aspect-ratio/utils/get-merged-styles'
+
+const AspectRatio = React.forwardRef((props, ref) => {
+ const { asChild, ratio, className, style, ...restProps } = props
+
+ const { mergedClassName, mergedStyle } = getMergedStyles(ratio, className, style)
+
+ const Component = polymorphic(asChild, 'div')
+
+ return
+})
+
+AspectRatio.displayName = 'AspectRatio'
+
+export { AspectRatio }
diff --git a/src/components/aspect-ratio/index.ts b/src/components/aspect-ratio/index.ts
new file mode 100644
index 0000000..04f2c27
--- /dev/null
+++ b/src/components/aspect-ratio/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/aspect-ratio/components/aspect-ratio'
+export * from '@/components/aspect-ratio/types/aspect-ratio'
diff --git a/src/components/aspect-ratio/types/aspect-ratio.ts b/src/components/aspect-ratio/types/aspect-ratio.ts
new file mode 100644
index 0000000..33979cd
--- /dev/null
+++ b/src/components/aspect-ratio/types/aspect-ratio.ts
@@ -0,0 +1,16 @@
+import { Simplify } from '@renderui/types'
+import React, { CSSProperties } from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type AspectRatioRef = React.ElementRef<'div'>
+
+type AspectRatioBoxProps = React.ComponentPropsWithoutRef<'div'>
+
+type AspectRatioRatioProps = {
+ ratio: CSSProperties['aspectRatio']
+}
+
+type AspectRatioProps = Simplify
+
+export type { AspectRatioProps, AspectRatioRef }
diff --git a/src/components/aspect-ratio/utils/get-merged-styles.ts b/src/components/aspect-ratio/utils/get-merged-styles.ts
new file mode 100644
index 0000000..8b06671
--- /dev/null
+++ b/src/components/aspect-ratio/utils/get-merged-styles.ts
@@ -0,0 +1,16 @@
+import { cn } from '@renderui/utils'
+
+import { AspectRatioProps } from '@/components/aspect-ratio/types/aspect-ratio'
+
+function getMergedStyles(
+ ratio: AspectRatioProps['ratio'],
+ className: AspectRatioProps['className'],
+ style: AspectRatioProps['style'],
+) {
+ return {
+ mergedClassName: cn('render-ui-aspect-ratio aspect-[var(--ratio)] overflow-hidden', className),
+ mergedStyle: ratio ? { '--ratio': ratio, ...style } : style,
+ }
+}
+
+export { getMergedStyles }
diff --git a/src/components/box/components/box.tsx b/src/components/box/components/box.tsx
new file mode 100644
index 0000000..c3d3731
--- /dev/null
+++ b/src/components/box/components/box.tsx
@@ -0,0 +1,19 @@
+import { polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { BoxProps, BoxRef } from '@/components/box/types/box'
+import { getMergedClassName } from '@/components/box/utils/get-merged-class-name'
+
+const Box = React.forwardRef((props, ref) => {
+ const { asChild, className, grow, noShrink, ...restProps } = props
+
+ const Component = polymorphic(asChild, 'div')
+
+ return (
+
+ )
+})
+
+Box.displayName = 'Box'
+
+export { Box }
diff --git a/src/components/box/index.ts b/src/components/box/index.ts
new file mode 100644
index 0000000..e5d2157
--- /dev/null
+++ b/src/components/box/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/box/components/box'
+export * from '@/components/box/types/box'
diff --git a/src/components/box/types/box.ts b/src/components/box/types/box.ts
new file mode 100644
index 0000000..6b644c8
--- /dev/null
+++ b/src/components/box/types/box.ts
@@ -0,0 +1,16 @@
+import { Simplify } from '@renderui/types'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type BoxRef = HTMLDivElement
+
+type BoxPrimitiveProps = React.ComponentPropsWithoutRef<'div'>
+
+type BoxCustomProps = {
+ grow?: boolean
+ noShrink?: boolean
+}
+
+type BoxProps = Simplify
+
+export type { BoxProps, BoxRef }
diff --git a/src/components/box/utils/get-merged-class-name.ts b/src/components/box/utils/get-merged-class-name.ts
new file mode 100644
index 0000000..a16d595
--- /dev/null
+++ b/src/components/box/utils/get-merged-class-name.ts
@@ -0,0 +1,13 @@
+import { cn } from '@renderui/utils'
+
+import { BoxProps } from '@/components/box/types/box'
+
+function getMergedClassName(
+ className: BoxProps['className'],
+ grow: BoxProps['grow'],
+ noShrink: BoxProps['noShrink'],
+) {
+ return cn('render-ui-box', grow ? 'grow' : '', noShrink ? 'shrink-0' : '', className)
+}
+
+export { getMergedClassName }
diff --git a/src/components/button/classes/button-classes.ts b/src/components/button/classes/button-classes.ts
new file mode 100644
index 0000000..83c2cd8
--- /dev/null
+++ b/src/components/button/classes/button-classes.ts
@@ -0,0 +1,104 @@
+import { cva } from '@renderui/utils'
+
+const buttonClasses = cva(
+ 'render-ui-button group relative box-border inline-flex cursor-pointer select-none appearance-none items-center justify-center gap-3 whitespace-nowrap rounded text-sm font-medium outline-none ring-ring-color ring-offset-background transition-[box-shadow,color,background-color,transform,opacity] duration-fast disabled:cursor-not-allowed disabled:opacity-40 data-[loading=true]:cursor-default [&_.render-ui-loader]:opacity-0',
+ {
+ variants: {
+ variant: {
+ plain: 'text-mode-foreground',
+ solid: '',
+ outline: '',
+ reveal: '',
+ text: 'data-[hover=true]:text-[rgba(var(--button-bg),0.8)]',
+ ghost: 'data-[hover=true]:bg-[rgba(var(--button-bg),0.15)]',
+ shadow: 'shadow-lg [&]:shadow-[rgba(var(--button-bg),0.5)]',
+ },
+ size: {
+ auto: '',
+ icon: 'apsect-square min-size-4 p-1',
+ sm: 'px-4.5 py-2 text-xs',
+ md: 'px-5 py-2.5 text-sm',
+ lg: 'px-7 py-3 text-base',
+ },
+ hasDefaultFocusVisibleStyles: {
+ true: 'data-[focus-visible=true]:ring-ring-width data-[focus-visible=true]:ring-offset-offset',
+ false: '',
+ },
+ hasRingOnAnyFocus: {
+ true: 'data-[focus=true]:ring-ring-width data-[focus=true]:ring-offset-offset',
+ false: '',
+ },
+ hasDefaultPressedStyles: {
+ true: 'data-[pressed=true]:scale-[0.97]',
+ false: '',
+ },
+ hasDefaultHoverStyles: {
+ true: '',
+ false: '',
+ },
+ hasLowerOpacityOnLoading: {
+ true: 'data-[loading=true]:opacity-70',
+ false: '',
+ },
+ hasLoaderOnLoading: {
+ true: '[&[data-loading=true]_.render-ui-loader]:opacity-100',
+ false: '',
+ },
+ hasContentOnLoading: {
+ true: '[&[data-loading=true]_.render-ui-loader]:flex [&_.render-ui-loader]:hidden',
+ false: [
+ '[&[data-loading=true]]:!text-transparent [&[data-loading=true]_*]:[transition:all_150ms,color_0s] [&[data-loading=true]_.render-ui-ripple]:opacity-100 [&[data-loading=true]_.render-ui-sub-layer]:opacity-100 [&[data-loading=true]_>_*]:opacity-0',
+ ],
+ },
+ },
+ compoundVariants: [
+ {
+ variant: ['solid', 'shadow', 'outline', 'reveal'],
+ className:
+ 'before:pointer-events-none before:inset-0 before:inline-block before:size-full before:rounded-[inherit] before:content-[""]',
+ },
+ {
+ variant: ['solid', 'shadow'],
+ className:
+ 'bg-[rgba(var(--button-bg))] text-[rgba(var(--button-color))] before:absolute before:z-[0] before:shadow-[shadow:inset_0_1px_theme(colors.white/15%)] after:pointer-events-none after:absolute after:inset-0 after:z-[0] after:inline-block after:size-full after:rounded-[inherit] after:ring after:ring-[0.5px] after:ring-inset after:ring-offset-[0px] after:content-[""] data-[color=mode-contrast]:before:hidden data-[color=mode]:before:hidden data-[color=mode-accent]:before:shadow-[shadow:inset_0_1px_theme(colors.white/10%)] data-[color=mode]:after:hidden [&]:after:ring-white/[5%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-[rgba(var(--button-bg),0.8)]',
+ },
+ {
+ variant: ['text', 'outline', 'ghost'],
+ className:
+ 'text-[rgba(var(--button-bg))] data-[pressed=true]:text-[rgba(var(--button-bg))]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ variant: ['outline', 'reveal'],
+ className:
+ 'data-[hover=true]:before:ring-[rgba(var(--button-bg))] data-[hover=true]:data-[pressed=true]:before:ring-[rgba(var(--button-bg))]',
+ },
+ {
+ variant: ['outline'],
+ className:
+ 'before:absolute before:ring before:ring-[1px] before:ring-inset before:ring-[rgba(var(--button-bg))] before:ring-offset-[0px] before:transition-[inherit] before:duration-[inherit]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ variant: ['outline'],
+ className: 'data-[hover=true]:bg-[rgba(var(--button-bg),0.1)]',
+ },
+ {
+ variant: ['reveal'],
+ className:
+ 'text-[rgba(var(--mode-accent-foreground))] before:absolute before:ring before:ring-[1px] before:ring-inset before:ring-[rgba(var(--mode-accent))] before:ring-offset-[0px] before:transition-[inherit] before:duration-[inherit] ',
+ },
+ {
+ variant: ['reveal'],
+ className: 'data-[hover=true]:text-[rgba(var(--button-bg))]',
+ },
+ ],
+ },
+)
+
+export { buttonClasses }
diff --git a/src/components/button/classes/button-variafnts.ts b/src/components/button/classes/button-variafnts.ts
new file mode 100644
index 0000000..c456322
--- /dev/null
+++ b/src/components/button/classes/button-variafnts.ts
@@ -0,0 +1,1720 @@
+import { cva } from '@renderui/utils'
+
+const sharedColorVariants = {
+ 'brand': [],
+ 'destructive': [],
+ 'help': [],
+ 'info': [],
+ 'mode': [],
+ 'mode-accent': [],
+ 'mode-contrast': [],
+ 'mode-contrast-accent': [],
+ 'neutral': [],
+ 'primary': [],
+ 'secondary': [],
+ 'success': [],
+ 'warning': [],
+} as const
+
+const buttonVariants = cva(['render-ui-button box-border'], {
+ variants: {
+ variant: {
+ unstyled: [],
+ base: [],
+ ghost: [],
+ blink: [],
+ solid: [],
+ shadow: [],
+ border: [],
+ business: [],
+ fill: [],
+ fade: [],
+ mute: [],
+ light: [],
+ twilight: [],
+ },
+ color: sharedColorVariants,
+ shadow: {
+ none: [],
+ sm: [],
+ base: [],
+ md: [],
+ lg: [],
+ xl: [],
+ inner: [],
+ },
+ fit: {
+ true: 'w-fit',
+ false: '',
+ },
+ textShadow: {},
+ size: {
+ xs: ['py-1', 'px-2', 'text-xs', 'font-medium'],
+ sm: ['py-1.5', 'px-3', 'text-xs', 'font-medium'],
+ base: ['py-2', 'px-4', 'text-sm', 'font-medium'],
+ md: ['py-2', 'px-5', 'text-base', 'font-medium'],
+ lg: ['py-2', 'px-8', 'text-lg', 'font-semibold'],
+ xl: ['py-2', 'px-10', 'text-xl', 'font-semibold'],
+ icon: ['p-0.5', 'min-w-[1.5rem]', 'h-fit', 'aspect-square', 'font-medium'],
+ auto: ['h-auto', 'w-auto', 'p-0', 'font-medium'],
+ },
+ shape: {
+ 'inline-flex': ['inline-flex', 'items-center', 'justify-center', 'gap-2'],
+ 'inline-block': ['inline-block', 'items-center', 'justify-center', 'gap-2'],
+ 'block': ['block', 'h-full', 'w-full'],
+ },
+ hasRingOnAnyFocus: {
+ false: [],
+ true: [],
+ },
+ hasDefaultHoverStyles: {
+ false: [],
+ true: [],
+ },
+ hasDefaultPressedStyles: {
+ false: [],
+ true: [],
+ },
+ hasDefaultFocusStyles: {
+ false: [],
+ true: [],
+ },
+ hasDefaultFocusVisibleStyles: {
+ false: [],
+ true: [],
+ },
+ hasContentOnLoading: {
+ true: [],
+ false: [],
+ },
+ hasLowerOpacityOnLoading: {
+ true: [],
+ false: [],
+ },
+ hasHoverEffectOnMobilePressed: {
+ true: [],
+ false: [],
+ },
+ isLoading: {
+ true: [],
+ false: [],
+ },
+ },
+ compoundVariants: [
+ // Foundation
+ {
+ className: [
+ 'render-ui-button',
+ 'group',
+ 'select-none',
+ 'subpixel-antialiased',
+ 'appearance-none',
+ ],
+ },
+
+ // Keyboard focus styles
+ {
+ hasDefaultFocusVisibleStyles: true,
+ className: [
+ 'outline-none',
+ 'ring-ring-color',
+ 'ring-offset-background',
+ 'data-[focus-visible=true]:ring-ring-width',
+ 'data-[focus-visible=true]:ring-offset-offset',
+ ],
+ },
+
+ // Defaults
+ {
+ className: [
+ 'relative',
+ 'overflow-hidden',
+ 'rounded',
+ 'transition-[box-shadow,background-color]',
+ 'duration-fast',
+ 'whitespace-nowrap',
+ 'cursor-pointer',
+ 'disabled:opacity-40',
+ 'disabled:cursor-not-allowed',
+ 'motion-reduce:transition-none',
+ 'data-[loading=true]:cursor-default',
+ ],
+ },
+ {
+ hasDefaultPressedStyles: true,
+ className: 'data-[pressed=true]:scale-active-pressed-scale',
+ },
+ {
+ hasLowerOpacityOnLoading: true,
+ className: ['data-[loading=true]:opacity-70'],
+ },
+ {
+ hasContentOnLoading: false,
+ isLoading: true,
+ className: [
+ '!text-transparent',
+ '[&_.render-ui-loader-dot]:visible',
+ '[&_.render-ui-loader]:visible',
+ '[&_.render-ui-ripple]:visible',
+ '[&_.render-ui-sub-layer]:visible',
+ '[&_>_*]:invisible',
+ '[&]:[transition:all_150ms,color_0s]',
+ '[&_*]:[transition:all_150ms,color_0s]',
+ ],
+ },
+ {
+ variant: ['border', 'fill', 'fade'],
+ className: [
+ 'before:content-[""]',
+ 'before:absolute',
+ 'before:inset-0',
+ 'before:inline-block',
+ 'before:ring-[1px]',
+ 'before:rounded-[inherit]',
+ 'before:ring-inset',
+ 'before:ring-offset-0',
+ ],
+ },
+ {
+ variant: ['shadow'],
+ className: 'shadow-lg',
+ },
+
+ // Shadow prop
+ {
+ shadow: ['none'],
+ className: 'shadow-none',
+ },
+ {
+ shadow: ['sm'],
+ className: 'shadow-sm',
+ },
+ {
+ shadow: ['base'],
+ className: 'shadow',
+ },
+ {
+ shadow: ['md'],
+ className: 'shadow-md',
+ },
+ {
+ shadow: ['lg'],
+ className: 'shadow-lg',
+ },
+ {
+ shadow: ['xl'],
+ className: 'shadow-xl',
+ },
+ {
+ shadow: ['inner'],
+ className: 'shadow-inner',
+ },
+
+ // Variant shared
+ {
+ variant: ['base', 'ghost', 'blink', 'border', 'fade'],
+ className: 'bg-transparent',
+ },
+ {
+ color: [
+ 'brand',
+ 'destructive',
+ 'help',
+ 'info',
+ 'mode',
+ 'mode-accent',
+ 'neutral',
+ 'primary',
+ 'secondary',
+ 'success',
+ 'warning',
+ ],
+ variant: ['fade', 'mute'],
+ className: ['bg-mode-accent'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: [
+ 'brand',
+ 'destructive',
+ 'help',
+ 'info',
+ 'mode',
+ 'mode-contrast',
+ 'mode-accent',
+ 'mode-contrast-accent',
+ 'neutral',
+ 'primary',
+ 'secondary',
+ 'success',
+ 'warning',
+ ],
+ variant: ['fade', 'mute'],
+ className: ['data-[hover=true]:bg-mode-accent/80'],
+ },
+ {
+ color: ['mode-contrast', 'mode-contrast-accent'],
+ variant: ['fade', 'mute'],
+ className: ['bg-mode-contrast-accent'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: ['mode-contrast', 'mode-contrast-accent'],
+ variant: ['fade', 'mute'],
+ className: ['data-[hover=true]:bg-mode-contrast-accent/80'],
+ },
+ {
+ variant: ['fade'],
+ className: ['border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ variant: ['mute'],
+ className: ['data-[hover=true]:border-neutral/40'],
+ },
+
+ // Mode variants
+ {
+ color: 'mode',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-mode/10'],
+ },
+ {
+ color: 'mode',
+ variant: ['shadow'],
+ className: ['shadow-mode/30'],
+ },
+ {
+ color: 'mode',
+ variant: ['border', 'fill'],
+ className: ['border-mode/70', 'data-[hover=true]:border-mode'],
+ },
+ {
+ color: 'mode',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-mode',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-mode/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-mode', 'data-[hover=true]:text-mode-foreground'],
+ },
+ {
+ color: 'mode',
+ variant: ['solid', 'shadow'],
+ className: ['bg-mode', 'text-mode-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: ['solid', 'shadow'],
+ className: ['data-[hover=true]:bg-mode/[90%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-mode/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-mode-foreground', 'data-[hover=true]:bg-mode'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: ['fade', 'mute'],
+ className: ['data-[hover=true]:text-mode/90'],
+ },
+ {
+ color: 'mode',
+ variant: 'light',
+ className: 'bg-mode/[15%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: 'light',
+ className: ['data-[hover=true]:bg-mode/20'],
+ },
+ {
+ color: 'mode',
+ variant: 'twilight',
+ className: ['bg-mode/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-mode-foreground', 'data-[hover=true]:bg-mode'],
+ },
+
+ // Mode contrast variants
+ {
+ color: 'mode-contrast',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-mode-contrast/10'],
+ },
+ {
+ color: 'mode-contrast',
+ variant: ['shadow'],
+ className: ['shadow-mode-contrast/30'],
+ },
+ {
+ color: 'mode-contrast',
+ variant: ['border', 'fill'],
+ className: ['border-mode-contrast/70', 'data-[hover=true]:border-mode-contrast'],
+ },
+ {
+ color: 'mode-contrast',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-mode-contrast',
+ },
+ {
+ color: 'mode-contrast',
+ variant: ['solid', 'shadow'],
+ className: 'text-mode-contrast-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-mode-contrast/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: 'blink',
+ className: [
+ 'data-[hover=true]:bg-mode-contrast',
+ 'data-[hover=true]:text-mode-contrast-foreground',
+ ],
+ },
+ {
+ color: 'mode-contrast',
+ variant: ['solid', 'shadow'],
+ className: ['bg-mode-contrast', 'text-mode-contrast-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: ['solid', 'shadow'],
+ className: ['data-[hover=true]:bg-mode-contrast/[90%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-mode-contrast/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: 'fill',
+ className: [
+ 'data-[hover=true]:text-mode-contrast-foreground',
+ 'data-[hover=true]:bg-mode-contrast',
+ ],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: ['fade', 'mute'],
+ className: ['data-[hover=true]:text-mode-contrast/90'],
+ },
+ {
+ color: 'mode-contrast',
+ variant: 'light',
+ className: 'bg-mode-contrast/[15%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-mode-contrast/[20%]',
+ },
+ {
+ color: 'mode-contrast',
+ variant: 'twilight',
+ className: ['bg-mode-contrast/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast',
+ variant: 'twilight',
+ className: [
+ 'data-[hover=true]:text-mode-contrast-foreground',
+ 'data-[hover=true]:bg-mode-contrast',
+ ],
+ },
+
+ // Mode accent variants
+ {
+ color: 'mode-accent',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-mode-contrast/10'],
+ },
+ {
+ color: 'mode-accent',
+ variant: ['shadow'],
+ className: ['shadow-mode-accent/30'],
+ },
+ {
+ color: 'mode-accent',
+ variant: ['border', 'fill'],
+ className: ['border-mode-accent/70', 'data-[hover=true]:border-mode-accent'],
+ },
+ {
+ color: 'mode-accent',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'light', 'twilight'],
+ className: 'text-mode-accent',
+ },
+ {
+ color: 'mode-accent',
+ variant: ['solid', 'shadow'],
+ className: 'text-mode-accent-foreground',
+ },
+ {
+ variant: ['fade', 'mute'],
+ className: 'text-mode',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-mode-accent/10'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'blink',
+ className: [
+ 'data-[hover=true]:bg-mode-accent',
+ 'data-[hover=true]:text-mode-accent-foreground',
+ ],
+ },
+ {
+ color: 'mode-accent',
+ variant: ['solid', 'shadow'],
+ className: ['bg-mode-accent', 'text-mode-accent-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: ['solid', 'shadow'],
+ className: ['data-[hover=true]:bg-mode-accent/90'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-mode-accent/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'fill',
+ className: [
+ 'data-[hover=true]:text-mode-accent-foreground',
+ 'data-[hover=true]:bg-mode-accent',
+ ],
+ },
+ {
+ color: 'mode-accent',
+ variant: ['fade', 'mute'],
+ className: ['bg-mode-accent'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: ['fade', 'mute'],
+ className: ['data-[hover=true]:text-mode/90', 'data-[hover=true]:bg-mode-accent/90'],
+ },
+ {
+ color: 'mode-accent',
+ variant: 'fade',
+ className: ['border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'fade',
+ className: ['data-[hover=true]:border-neutral/[40%]'],
+ },
+ {
+ color: 'mode-accent',
+ variant: 'light',
+ className: 'bg-mode-accent/[15%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-mode-accent/20',
+ },
+ {
+ color: 'mode-accent',
+ variant: 'twilight',
+ className: ['bg-mode-accent/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-accent',
+ variant: 'twilight',
+ className: [
+ 'data-[hover=true]:text-mode-accent-foreground',
+ 'data-[hover=true]:bg-mode-accent',
+ ],
+ },
+
+ // Mode contrast accent variants
+ {
+ color: 'mode-contrast-accent',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-mode-contrast-accent/10'],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: ['shadow'],
+ className: ['shadow-mode-contrast-accent/30'],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: ['border', 'fill'],
+ className: [
+ 'border-mode-contrast-accent/70',
+ 'data-[hover=true]:border-mode-contrast-accent',
+ ],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-mode-contrast-accent',
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: ['solid', 'shadow'],
+ className: 'text-mode-contrast-accent-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-mode-contrast-accent/10'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: 'blink',
+ className: [
+ 'data-[hover=true]:bg-mode-contrast-accent',
+ 'data-[hover=true]:text-mode-contrast-accent-foreground',
+ ],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: ['solid', 'shadow'],
+ className: ['bg-mode-contrast-accent', 'text-mode-contrast-accent-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: ['solid', 'shadow'],
+ className: ['data-[hover=true]:bg-mode-contrast-accent/90'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-mode-contrast-accent/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: 'fill',
+ className: [
+ 'data-[hover=true]:text-mode-contrast-accent-foreground',
+ 'data-[hover=true]:bg-mode-contrast-accent',
+ ],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: ['fade', 'mute'],
+ className: [
+ 'bg-mode-accent',
+ 'data-[hover=true]:text-mode-contrast/90',
+ 'data-[hover=true]:bg-mode-accent/90',
+ ],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: 'fade',
+ className: ['border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: 'fade',
+ className: ['data-[hover=true]:border-neutral/[40%]'],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: 'light',
+ className: ['bg-mode-contrast-accent/[15%]'],
+ },
+ {
+ color: 'mode-contrast-accent',
+ variant: 'twilight',
+ className: ['bg-mode-contrast-accent/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'mode-contrast-accent',
+ variant: 'twilight',
+ className: [
+ 'data-[hover=true]:text-mode-contrast-accent-foreground',
+ 'data-[hover=true]:bg-mode-contrast-accent',
+ ],
+ },
+
+ // Neutral variants
+ {
+ color: 'neutral',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-neutral/10'],
+ },
+ {
+ color: 'neutral',
+ variant: ['shadow'],
+ className: ['shadow-neutral/30'],
+ },
+ {
+ color: 'neutral',
+ variant: ['border', 'fill'],
+ className: ['border-neutral/70', 'data-[hover=true]:border-neutral'],
+ },
+ {
+ color: 'neutral',
+ variant: ['base', 'ghost', 'blink', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-neutral',
+ },
+ {
+ color: 'neutral',
+ variant: ['solid', 'shadow', 'border', 'fill'],
+ className: 'text-neutral-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-neutral/10'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-neutral', 'data-[hover=true]:text-neutral-foreground'],
+ },
+ {
+ color: 'neutral',
+ variant: ['solid', 'shadow'],
+ className: ['bg-neutral', 'text-neutral-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-neutral/90',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-neutral/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-neutral-foreground', 'data-[hover=true]:bg-neutral'],
+ },
+ {
+ color: 'neutral',
+ variant: 'fade',
+ className: ['bg-neutral-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-neutral/[75%]',
+ 'data-[hover=true]:bg-neutral-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'neutral',
+ variant: 'mute',
+ className: ['bg-neutral-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'mute',
+ className: [
+ 'data-[hover=true]:text-neutral/[75%]',
+ 'data-[hover=true]:bg-neutral-accent/[45%]',
+ ],
+ },
+ {
+ color: 'neutral',
+ variant: 'light',
+ className: 'bg-neutral/[15%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-neutral/20',
+ },
+ {
+ color: 'neutral',
+ variant: 'twilight',
+ className: ['bg-neutral/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'neutral',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-neutral-foreground', 'data-[hover=true]:bg-neutral'],
+ },
+
+ // Primary variants
+ {
+ color: 'primary',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-primary/10'],
+ },
+ {
+ color: 'primary',
+ variant: ['shadow'],
+ className: ['shadow-primary/30'],
+ },
+ {
+ color: 'primary',
+ variant: ['border', 'fill'],
+ className: ['before:ring-primary/70', 'data-[hover=true]:before:ring-primary'],
+ },
+ {
+ color: 'primary',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-primary',
+ },
+ {
+ color: 'primary',
+ variant: ['solid', 'shadow'],
+ className: 'text-primary-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-primary/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-primary', 'data-[hover=true]:text-primary-foreground'],
+ },
+ {
+ color: 'primary',
+ variant: ['solid', 'shadow'],
+ className: ['bg-primary', 'text-primary-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-primary/90',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-primary/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-primary-foreground', 'data-[hover=true]:bg-primary'],
+ },
+ {
+ color: 'primary',
+ variant: 'fade',
+ className: ['bg-mode-accent', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-primary/90',
+ 'data-[hover=true]:bg-mode-accent/90',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'primary',
+ variant: 'mute',
+ className: ['bg-mode-accent'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'mute',
+ className: ['data-[hover=true]:text-primary/90', 'data-[hover=true]:bg-mode-accent/90'],
+ },
+ {
+ color: 'primary',
+ variant: 'light',
+ className: 'bg-primary/[15%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-primary/20',
+ },
+ {
+ color: 'primary',
+ variant: 'twilight',
+ className: ['bg-primary/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'primary',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-primary-foreground', 'data-[hover=true]:bg-primary'],
+ },
+
+ // Secondary variants
+ {
+ color: 'secondary',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-secondary/10'],
+ },
+ {
+ color: 'secondary',
+ variant: ['shadow'],
+ className: ['shadow-secondary/30'],
+ },
+ {
+ color: 'secondary',
+ variant: ['border', 'fill'],
+ className: ['before:ring-secondary/70', 'data-[hover=true]:before:ring-secondary'],
+ },
+ {
+ color: 'secondary',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-secondary',
+ },
+ {
+ color: 'secondary',
+ variant: ['solid', 'shadow'],
+ className: 'text-secondary-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-secondary/10'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-secondary', 'data-[hover=true]:text-secondary-foreground'],
+ },
+ {
+ color: 'secondary',
+ variant: ['solid', 'shadow'],
+ className: ['bg-secondary', 'text-secondary-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: ['solid', 'shadow'],
+ className: ['data-[hover=true]:bg-secondary/90'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-secondary/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-secondary-foreground', 'data-[hover=true]:bg-secondary'],
+ },
+ {
+ color: 'secondary',
+ variant: 'fade',
+ className: ['bg-mode-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-secondary/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'secondary',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'mute',
+ className: [
+ 'data-[hover=true]:text-secondary/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[45%]',
+ ],
+ },
+ {
+ color: 'secondary',
+ variant: 'light',
+ className: 'bg-secondary/[15%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-secondary/20',
+ },
+ {
+ color: 'secondary',
+ variant: 'twilight',
+ className: ['bg-secondary/[15%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'secondary',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-secondary-foreground', 'data-[hover=true]:bg-secondary'],
+ },
+
+ // Destructive variants
+ {
+ color: 'destructive',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-destructive/10'],
+ },
+ {
+ color: 'destructive',
+ variant: ['shadow'],
+ className: ['shadow-destructive/30'],
+ },
+ {
+ color: 'destructive',
+ variant: ['border', 'fill'],
+ className: ['before:ring-destructive/70', 'data-[hover=true]:before:ring-destructive'],
+ },
+ {
+ color: 'destructive',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-destructive',
+ },
+ {
+ color: 'destructive',
+ variant: ['solid', 'shadow'],
+ className: 'text-destructive-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-destructive/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'blink',
+ className: [
+ 'data-[hover=true]:bg-destructive',
+ 'data-[hover=true]:text-destructive-foreground',
+ ],
+ },
+ {
+ color: 'destructive',
+ variant: ['solid', 'shadow'],
+ className: ['bg-destructive', 'text-destructive-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: ['solid', 'shadow'],
+ className: ['data-[hover=true]:bg-destructive/[85%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-destructive/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'fill',
+ className: [
+ 'data-[hover=true]:text-destructive-foreground',
+ 'data-[hover=true]:bg-destructive',
+ ],
+ },
+ {
+ color: 'destructive',
+ variant: 'fade',
+ className: ['bg-mode-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-destructive/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'destructive',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'mute',
+ className: [
+ 'data-[hover=true]:text-destructive/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[45%]',
+ ],
+ },
+ {
+ color: 'destructive',
+ variant: 'light',
+ className: 'bg-destructive/20',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-destructive/30',
+ },
+ {
+ color: 'destructive',
+ variant: 'twilight',
+ className: ['bg-destructive/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'destructive',
+ variant: 'twilight',
+ className: [
+ 'data-[hover=true]:text-destructive-foreground',
+ 'data-[hover=true]:bg-destructive',
+ ],
+ },
+
+ // Success variants
+ {
+ color: 'success',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-success/10'],
+ },
+ {
+ color: 'success',
+ variant: ['shadow'],
+ className: ['shadow-success/30'],
+ },
+ {
+ color: 'success',
+ variant: ['border', 'fill'],
+ className: ['before:ring-success/70', 'data-[hover=true]:before:ring-success'],
+ },
+ {
+ color: 'success',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-success',
+ },
+ {
+ color: 'success',
+ variant: ['solid', 'shadow'],
+ className: 'text-success-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-success/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-success', 'data-[hover=true]:text-success-foreground'],
+ },
+ {
+ color: 'success',
+ variant: ['solid', 'shadow'],
+ className: ['bg-success', 'text-success-foreground', 'data-[hover=true]:bg-success/[85%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-success/[85%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-success/10'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-success-foreground', 'data-[hover=true]:bg-success'],
+ },
+ {
+ color: 'success',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-success/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-success/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'success',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'mute',
+ className: ['data-[hover=true]:text-success/[75%]', 'data-[hover=true]:bg-mode-accent/[45%]'],
+ },
+ {
+ color: 'success',
+ variant: 'light',
+ className: ['bg-success/20', 'data-[hover=true]:bg-success/30'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-success/30',
+ },
+ {
+ color: 'success',
+ variant: 'twilight',
+ className: ['bg-success/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'success',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-success-foreground', 'data-[hover=true]:bg-success'],
+ },
+
+ // Warning variants
+ {
+ color: 'warning',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-warning/10'],
+ },
+ {
+ color: 'warning',
+ variant: ['shadow'],
+ className: ['shadow-warning/30'],
+ },
+ {
+ color: 'warning',
+ variant: ['border', 'fill'],
+ className: ['before:ring-warning/70', 'data-[hover=true]:before:ring-warning'],
+ },
+ {
+ color: 'warning',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-warning',
+ },
+ {
+ color: 'warning',
+ variant: ['solid', 'shadow'],
+ className: 'text-warning-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-warning/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-warning', 'data-[hover=true]:text-warning-foreground'],
+ },
+ {
+ color: 'warning',
+ variant: ['solid', 'shadow'],
+ className: ['bg-warning', 'text-warning-foreground'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-warning/[85%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-warning/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-warning-foreground', 'data-[hover=true]:bg-warning'],
+ },
+ {
+ color: 'warning',
+ variant: 'fade',
+ className: ['bg-mode-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-warning/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'warning',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'mute',
+ className: ['data-[hover=true]:text-warning/[75%]', 'data-[hover=true]:bg-mode-accent/[45%]'],
+ },
+ {
+ color: 'warning',
+ variant: 'light',
+ className: ['bg-warning/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-warning/30',
+ },
+ {
+ color: 'warning',
+ variant: 'twilight',
+ className: ['bg-warning/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'warning',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-warning-foreground', 'data-[hover=true]:bg-warning'],
+ },
+
+ // Info variants
+ {
+ color: 'info',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-info/10'],
+ },
+ {
+ color: 'info',
+ variant: ['shadow'],
+ className: ['shadow-info/30'],
+ },
+ {
+ color: 'info',
+ variant: ['border', 'fill'],
+ className: ['before:ring-info/70', 'data-[hover=true]:before:ring-info'],
+ },
+ {
+ color: 'info',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-info',
+ },
+ {
+ color: 'info',
+ variant: ['solid', 'shadow'],
+ className: 'text-info-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-info/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-info', 'data-[hover=true]:text-info-foreground'],
+ },
+ {
+ color: 'info',
+ variant: ['solid', 'shadow'],
+ className: ['bg-info', 'text-info-foreground', 'data-[hover=true]:bg-info/[85%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-info/[85%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-info/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-info-foreground', 'data-[hover=true]:bg-info'],
+ },
+ {
+ color: 'info',
+ variant: 'fade',
+ className: ['bg-mode-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-info/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'info',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'mute',
+ className: ['data-[hover=true]:text-info/[75%]', 'data-[hover=true]:bg-mode-accent/[45%]'],
+ },
+ {
+ color: 'info',
+ variant: 'light',
+ className: 'bg-info/20',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-info/30',
+ },
+ {
+ color: 'info',
+ variant: 'twilight',
+ className: ['bg-info/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'info',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-info-foreground', 'data-[hover=true]:bg-info'],
+ },
+
+ // Help variants
+ {
+ color: 'help',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-help/10'],
+ },
+ {
+ color: 'help',
+ variant: ['shadow'],
+ className: ['shadow-help/30'],
+ },
+ {
+ color: 'help',
+ variant: ['border', 'fill'],
+ className: ['before:ring-help/70', 'data-[hover=true]:before:ring-help'],
+ },
+ {
+ color: 'help',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-help',
+ },
+ {
+ color: 'help',
+ variant: ['solid', 'shadow'],
+ className: 'text-help-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-help/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-help', 'data-[hover=true]:text-help-foreground'],
+ },
+ {
+ color: 'help',
+ variant: ['solid', 'shadow'],
+ className: ['bg-help', 'text-help-foreground', 'data-[hover=true]:bg-help/[85%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-help/[85%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-help/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-help-foreground', 'data-[hover=true]:bg-help'],
+ },
+ {
+ color: 'help',
+ variant: 'fade',
+ className: ['bg-mode-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-help/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'help',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'mute',
+ className: ['data-[hover=true]:text-help/[75%]', 'data-[hover=true]:bg-mode-accent/[45%]'],
+ },
+ {
+ color: 'help',
+ variant: 'light',
+ className: 'bg-help/20',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-help/30',
+ },
+ {
+ color: 'help',
+ variant: 'twilight',
+ className: ['bg-help/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'help',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-help-foreground', 'data-[hover=true]:bg-help'],
+ },
+
+ // Brand variants
+ {
+ color: 'brand',
+ variant: ['solid', 'border', 'fill', 'light', 'twilight'],
+ className: ['shadow-brand/10'],
+ },
+ {
+ color: 'brand',
+ variant: ['shadow'],
+ className: ['shadow-brand/30'],
+ },
+ {
+ color: 'brand',
+ variant: ['border', 'fill'],
+ className: ['before:ring-brand/70', 'data-[hover=true]:before:ring-brand'],
+ },
+ {
+ color: 'brand',
+ variant: ['base', 'ghost', 'blink', 'border', 'fill', 'fade', 'mute', 'light', 'twilight'],
+ className: 'text-brand',
+ },
+ {
+ color: 'brand',
+ variant: ['solid', 'shadow'],
+ className: 'text-brand-foreground',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'ghost',
+ className: ['data-[hover=true]:bg-brand/10'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'blink',
+ className: ['data-[hover=true]:bg-brand', 'data-[hover=true]:text-brand-foreground'],
+ },
+ {
+ color: 'brand',
+ variant: ['solid', 'shadow'],
+ className: ['bg-brand', 'text-brand-foreground', 'data-[hover=true]:bg-brand/[85%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: ['solid', 'shadow'],
+ className: 'data-[hover=true]:bg-brand/[85%]',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'border',
+ className: ['data-[hover=true]:bg-brand/[5%]'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'fill',
+ className: ['data-[hover=true]:text-brand-foreground', 'data-[hover=true]:bg-brand'],
+ },
+ {
+ color: 'brand',
+ variant: 'fade',
+ className: ['bg-mode-accent/50', 'border-neutral/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'fade',
+ className: [
+ 'data-[hover=true]:text-brand/[75%]',
+ 'data-[hover=true]:bg-mode-accent/[40%]',
+ 'data-[hover=true]:border-neutral/[40%]',
+ ],
+ },
+ {
+ color: 'brand',
+ variant: 'mute',
+ className: ['bg-mode-accent/50'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'mute',
+ className: ['data-[hover=true]:text-brand/[75%]', 'data-[hover=true]:bg-mode-accent/[45%]'],
+ },
+ {
+ color: 'brand',
+ variant: 'light',
+ className: 'bg-brand/20',
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'light',
+ className: 'data-[hover=true]:bg-brand/30',
+ },
+ {
+ color: 'brand',
+ variant: 'twilight',
+ className: ['bg-brand/20'],
+ },
+ {
+ hasDefaultHoverStyles: true,
+ color: 'brand',
+ variant: 'twilight',
+ className: ['data-[hover=true]:text-brand-foreground', 'data-[hover=true]:bg-brand'],
+ },
+ {
+ hasRingOnAnyFocus: true,
+ className: ['data-[focus=true]:ring-ring-width', 'data-[focus=true]:ring-offset-offset'],
+ },
+ ],
+ defaultVariants: {
+ color: 'mode-contrast',
+ variant: 'solid',
+ shape: 'inline-flex',
+ size: 'base',
+ hasDefaultPressedStyles: true,
+ hasDefaultFocusStyles: true,
+ hasDefaultHoverStyles: true,
+ hasDefaultFocusVisibleStyles: true,
+ hasRingOnAnyFocus: false,
+ hasLowerOpacityOnLoading: false,
+ hasContentOnLoading: true,
+ },
+})
+
+export { buttonVariants }
diff --git a/src/components/button/components/button.tsx b/src/components/button/components/button.tsx
new file mode 100644
index 0000000..1a4b970
--- /dev/null
+++ b/src/components/button/components/button.tsx
@@ -0,0 +1,56 @@
+'use client'
+
+import { functionCallOrValue, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { useButton } from '@/components/button/hooks/use-button'
+import { useLazyComponents } from '@/components/button/hooks/use-lazy-components'
+import { ButtonProps, ButtonRef } from '@/components/button/types/button'
+
+const Button = React.forwardRef((props, ref) => {
+ const { buttonProps, rippleProps, loaderProps, utility } = useButton(props, ref)
+ const { children } = buttonProps
+ const {
+ asChild,
+ isPressed,
+ isKeyboardPressed,
+ isLoading,
+ loader,
+ loaderPosition,
+ hasLoaderOnLoading,
+ startContent,
+ endContent,
+ loadingContent,
+ hasRipple,
+ } = utility
+
+ const { LoaderComponent, RippleComponent } = useLazyComponents({
+ isLoading,
+ hasLoaderOnLoading,
+ loader,
+ hasRipple,
+ })
+
+ const Component = polymorphic(asChild, 'button')
+
+ return (
+
+ {functionCallOrValue(startContent, { isPressed, isKeyboardPressed })}
+ {isLoading && loaderPosition === 'start' && LoaderComponent ? (
+
+ ) : null}
+ {isLoading && loadingContent
+ ? functionCallOrValue(loadingContent, { isPressed, isKeyboardPressed })
+ : functionCallOrValue(children, { isPressed, isKeyboardPressed })}
+ {isLoading && loaderPosition !== 'start' && LoaderComponent ? (
+
+ ) : null}
+ {functionCallOrValue(endContent, { isPressed, isKeyboardPressed })}
+ {RippleComponent ? : null}
+
+ )
+})
+
+Button.displayName = 'Button'
+
+export { Button }
diff --git a/src/components/button/hooks/use-button.ts b/src/components/button/hooks/use-button.ts
new file mode 100644
index 0000000..3ad6bf2
--- /dev/null
+++ b/src/components/button/hooks/use-button.ts
@@ -0,0 +1,131 @@
+import { useMergedRef } from '@renderui/hooks'
+import { chain, cn } from '@renderui/utils'
+import React from 'react'
+
+import { useAriaHandlers } from '@/components/_shared/hooks/use-aria-handlers'
+import { splitAriaProps } from '@/components/_shared/utils/split-aria-props'
+import { buttonClasses } from '@/components/button/classes/button-classes'
+import { useInitialStyle } from '@/components/button/hooks/use-initial-style'
+import { ButtonProps, ButtonRef } from '@/components/button/types/button'
+import { getColorVariables } from '@/components/button/utils/get-color-variables'
+import { getLoaderProps } from '@/components/button/utils/get-loader-props'
+import { getRippleProps } from '@/components/button/utils/get-ripple-props'
+
+function useButton(props: ButtonProps, ref: React.ForwardedRef) {
+ const { ariaProps, nonAriaProps } = splitAriaProps(props)
+ const {
+ asChild,
+ className,
+ style,
+ isDisabled,
+ isLoading,
+ startContent,
+ endContent,
+ loadingContent,
+ loaderProps,
+ loaderRef,
+ rippleProps,
+ rippleRef,
+ loader,
+ type = 'button',
+ size = 'md',
+ variant = 'solid',
+ color = 'primary',
+ loaderPosition = 'end',
+ hasLoaderOnLoading = true,
+ hasRipple = true,
+ hasDefaultHoverStyles = true,
+ hasDefaultFocusVisibleStyles = true,
+ hasDefaultPressedStyles = true,
+ hasRingOnAnyFocus = false,
+ hasLowerOpacityOnLoading = false,
+ hasContentOnLoading = true,
+ onClick,
+ ...restProps
+ } = nonAriaProps
+
+ const internalRef = React.useRef(null)
+ const mergedRefs = useMergedRef([internalRef, ref])
+
+ const initialColorDependency = React.useMemo(() => [variant, color], [variant, color])
+ const initialColor = useInitialStyle(internalRef, initialColorDependency)
+
+ const isPressDisabled = isDisabled || ariaProps?.isPressDisabled || isLoading
+ const isHoverDisabled = isDisabled || ariaProps?.isHoverDisabled || isLoading
+
+ const { ariaComponentProps, ariaFlags } = useAriaHandlers(
+ {
+ ...ariaProps,
+ onPress: chain(ariaProps.onPress, onClick),
+ isPressDisabled,
+ isHoverDisabled,
+ },
+ internalRef,
+ )
+ const { isPressed, isKeyboardPressed } = ariaFlags
+
+ const mergedLoaderProps = getLoaderProps({ loaderRef, loaderProps, initialColor, isLoading })
+
+ const hasContent = () => {
+ if (mergedLoaderProps.position === 'absolute-center') return false
+
+ return hasContentOnLoading
+ }
+
+ const memoizedStyleWithColorVariable = React.useMemo(
+ () => ({
+ ...style,
+ ...getColorVariables(color),
+ }),
+ [style, color],
+ )
+
+ return {
+ buttonProps: {
+ type,
+ 'ref': mergedRefs,
+ 'disabled': isDisabled,
+ 'className': cn(
+ buttonClasses({
+ size,
+ variant,
+ hasRingOnAnyFocus,
+ hasDefaultFocusVisibleStyles,
+ hasDefaultPressedStyles,
+ hasDefaultHoverStyles,
+ hasLowerOpacityOnLoading,
+ hasLoaderOnLoading,
+ hasContentOnLoading: hasContent(),
+ }),
+ className,
+ ),
+ 'style': memoizedStyleWithColorVariable,
+ 'aria-label': isLoading ? 'loading' : undefined,
+ 'aria-busy': isLoading || undefined,
+ 'data-variant': variant,
+ 'data-color': color,
+ 'data-loading': isLoading || undefined,
+ 'data-disabled': isDisabled,
+ 'data-slot': 'base',
+ ...ariaComponentProps,
+ ...restProps,
+ },
+ rippleProps: getRippleProps({ rippleRef, rippleProps, initialColor, isLoading }),
+ loaderProps: mergedLoaderProps,
+ utility: {
+ asChild,
+ isPressed,
+ isKeyboardPressed,
+ isLoading,
+ startContent,
+ endContent,
+ loadingContent,
+ loader,
+ loaderPosition,
+ hasRipple,
+ hasLoaderOnLoading,
+ },
+ }
+}
+
+export { useButton }
diff --git a/src/components/button/hooks/use-initial-style.ts b/src/components/button/hooks/use-initial-style.ts
new file mode 100644
index 0000000..acb1dfb
--- /dev/null
+++ b/src/components/button/hooks/use-initial-style.ts
@@ -0,0 +1,17 @@
+import React from 'react'
+
+const useInitialStyle = (ref: React.RefObject, dependency?: unknown) => {
+ const [style, setStyle] = React.useState()
+
+ React.useEffect(() => {
+ if (!ref?.current) return
+
+ const initialStyle = getComputedStyle(ref.current).color
+
+ setStyle(initialStyle)
+ }, [ref, dependency])
+
+ return style
+}
+
+export { useInitialStyle }
diff --git a/src/components/button/hooks/use-lazy-components.ts b/src/components/button/hooks/use-lazy-components.ts
new file mode 100644
index 0000000..327de82
--- /dev/null
+++ b/src/components/button/hooks/use-lazy-components.ts
@@ -0,0 +1,42 @@
+/* eslint-disable react/hook-use-state */
+import React from 'react'
+
+import { ButtonProps } from '@/components/button/types/button'
+
+type UseLazyComponentsProps = Pick<
+ ButtonProps,
+ 'isLoading' | 'hasLoaderOnLoading' | 'loader' | 'hasRipple'
+>
+
+function useLazyComponents(props: UseLazyComponentsProps) {
+ const { hasLoaderOnLoading, loader, hasRipple } = props
+
+ const [LoaderComponent, setLoaderComponent] = React.useState(null)
+ const [RippleComponent, setRippleComponent] = React.useState(null)
+
+ React.useEffect(() => {
+ const loadComponents = async () => {
+ if (hasLoaderOnLoading) {
+ if (loader) {
+ setLoaderComponent(loader)
+ } else {
+ const { Loader } = await import('@/components/loader')
+
+ setLoaderComponent(Loader)
+ }
+ }
+
+ if (hasRipple) {
+ const { Ripple } = await import('@/components/ripple')
+
+ setRippleComponent(Ripple)
+ }
+ }
+
+ loadComponents()
+ }, [loader, hasLoaderOnLoading, hasRipple])
+
+ return { LoaderComponent, RippleComponent }
+}
+
+export { useLazyComponents }
diff --git a/src/components/button/index.ts b/src/components/button/index.ts
new file mode 100644
index 0000000..87055d7
--- /dev/null
+++ b/src/components/button/index.ts
@@ -0,0 +1,3 @@
+export * from '@/components/button/classes/button-classes'
+export * from '@/components/button/components/button'
+export * from '@/components/button/types/button'
diff --git a/src/components/button/types/button.ts b/src/components/button/types/button.ts
new file mode 100644
index 0000000..bbddf7e
--- /dev/null
+++ b/src/components/button/types/button.ts
@@ -0,0 +1,49 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { OptionalAriaProps } from '@/components/_shared/types/aria'
+import { AsChildProp } from '@/components/_shared/types/as-child'
+import { Color } from '@/components/_shared/types/colors'
+import { NonNullableVariantProps } from '@/components/_shared/types/variants'
+import { buttonClasses } from '@/components/button/classes/button-classes'
+import { LoaderProps, LoaderRef } from '@/components/loader'
+import { RippleProps, RippleRef } from '@/components/ripple'
+
+type ButtonRef = React.ElementRef<'button'>
+
+type ButtonPrimitiveProps = Omit<
+ React.ComponentPropsWithoutRef<'button'>,
+ 'children' | 'disabled' | 'color'
+>
+
+type ButtonRenderPropsProps = {
+ isPressed?: boolean
+ isKeyboardPressed?: boolean
+}
+
+type ButtonRenderProps = ((props: ButtonRenderPropsProps) => React.ReactNode) | React.ReactNode
+
+type ButtonCustomProps = {
+ children?: ButtonRenderProps
+ startContent?: ButtonRenderProps
+ endContent?: ButtonRenderProps
+ loadingContent?: ButtonRenderProps
+ hasRipple?: boolean
+ isDisabled?: boolean
+ isLoading?: boolean
+ color?: Color
+ loaderPosition?: 'start' | 'end'
+ loaderProps?: LoaderProps
+ loaderRef?: React.Ref
+ loader?: React.ReactNode
+ rippleProps?: RippleProps
+ rippleRef?: React.Ref
+}
+
+type ButtoVariantProps = NonNullableVariantProps
+
+type ButtonProps = Simplify<
+ ButtonPrimitiveProps & ButtonCustomProps & ButtoVariantProps & OptionalAriaProps & AsChildProp
+>
+
+export type { ButtonProps, ButtonRef }
diff --git a/src/components/button/utils/get-color-variables.ts b/src/components/button/utils/get-color-variables.ts
new file mode 100644
index 0000000..189d365
--- /dev/null
+++ b/src/components/button/utils/get-color-variables.ts
@@ -0,0 +1,11 @@
+import React from 'react'
+
+import { Color } from '@/components/_shared/types/colors'
+
+const getColorVariables = (color: Color) =>
+ ({
+ '--button-bg': `var(--${color})`,
+ '--button-color': `var(--${color}-foreground)`,
+ }) as React.CSSProperties
+
+export { getColorVariables }
diff --git a/src/components/button/utils/get-loader-props.ts b/src/components/button/utils/get-loader-props.ts
new file mode 100644
index 0000000..6a4f61d
--- /dev/null
+++ b/src/components/button/utils/get-loader-props.ts
@@ -0,0 +1,38 @@
+import { getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import { LoaderProps } from '@/components/loader'
+
+type GetLoaderPropsArgs = {
+ loaderRef: React.Ref | undefined
+ loaderProps: LoaderProps | undefined
+ initialColor: string | undefined
+ isLoading: boolean | undefined
+}
+
+const getLoaderProps = (args: GetLoaderPropsArgs) => {
+ const { loaderRef, loaderProps, initialColor, isLoading } = args
+
+ const {
+ style: styleProp,
+ position = 'absolute-center',
+ ...restLoaderProps
+ } = getOptionalObject(loaderProps)
+
+ const style = isLoading
+ ? {
+ color: initialColor,
+ ...styleProp,
+ }
+ : undefined
+
+ return {
+ 'ref': loaderRef,
+ 'data-slot': 'loader',
+ style,
+ position,
+ ...restLoaderProps,
+ }
+}
+
+export { getLoaderProps }
diff --git a/src/components/button/utils/get-ripple-props.ts b/src/components/button/utils/get-ripple-props.ts
new file mode 100644
index 0000000..4706c04
--- /dev/null
+++ b/src/components/button/utils/get-ripple-props.ts
@@ -0,0 +1,43 @@
+import { getOptionalObject } from '@renderui/utils'
+
+import { RippleProps, RippleRef } from '@/components/ripple'
+
+// @TODO understand type problem and fix it, remove any's
+type GetRipplePropsArgs = {
+ rippleRef: React.Ref | undefined
+ rippleProps: RippleProps | undefined
+ initialColor: string | undefined
+ isLoading: boolean | undefined
+}
+
+const getRippleProps = (args: any): any => {
+ const { rippleRef, rippleProps, initialColor, isLoading } = args
+
+ const {
+ color: colorProp,
+ style: styleProp,
+ isDisabled: isDisabledProp,
+ ...restRippleProps
+ } = getOptionalObject(rippleProps)
+
+ const isDisabled = isLoading ?? isDisabledProp
+
+ const rippleColor = colorProp ? undefined : initialColor
+
+ const style = isLoading
+ ? {
+ backgroundColor: rippleColor,
+ ...styleProp,
+ }
+ : undefined
+
+ return {
+ 'ref': rippleRef,
+ 'data-slot': 'ripple',
+ style,
+ isDisabled,
+ ...restRippleProps,
+ }
+}
+
+export { getRippleProps }
diff --git a/src/components/card/classes/card-classes.ts b/src/components/card/classes/card-classes.ts
new file mode 100644
index 0000000..b0e0dba
--- /dev/null
+++ b/src/components/card/classes/card-classes.ts
@@ -0,0 +1,28 @@
+import { cva } from '@renderui/utils'
+
+const cardClasses = cva(
+ 'render-ui-card dark:border-mode-accent-high relative flex w-fit flex-col justify-between overflow-hidden rounded-lg border border-mode-accent bg-card text-foreground shadow-sm transition-all duration-fast',
+ {
+ variants: {
+ hasShadowOnHover: {
+ true: 'hover:shadow',
+ false: '',
+ },
+ isBlurred: {
+ true: ['bg-foreground/80', 'backdrop-blur-md', 'backdrop-saturate-150'],
+ false: [],
+ },
+ isPressable: {
+ true: ['data-[pressed=true]:scale-active-pressed-scale', 'data-[hover=true]:shadow'],
+ false: [],
+ },
+ },
+ defaultVariants: {
+ isBlurred: false,
+ isPressable: false,
+ hasShadowOnHover: false,
+ },
+ },
+)
+
+export { cardClasses }
diff --git a/src/components/card/components/card-body.tsx b/src/components/card/components/card-body.tsx
new file mode 100644
index 0000000..b8e948d
--- /dev/null
+++ b/src/components/card/components/card-body.tsx
@@ -0,0 +1,19 @@
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_CARD_BODY_CLASSNAME } from '@/components/card/constants/constants'
+import { CardBodyProps, CardBodyRef } from '@/components/card/types/card-body'
+
+const CardBody = React.forwardRef((props, ref) => {
+ const { asChild, className, ...restProps } = props
+
+ const Component = polymorphic(asChild, 'div')
+
+ return (
+
+ )
+})
+
+CardBody.displayName = 'CardBody'
+
+export { CardBody }
diff --git a/src/components/card/components/card-description.tsx b/src/components/card/components/card-description.tsx
new file mode 100644
index 0000000..9b52336
--- /dev/null
+++ b/src/components/card/components/card-description.tsx
@@ -0,0 +1,24 @@
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_CARD_DESCRIPTION_CLASSNAME } from '@/components/card/constants/constants'
+import { CardDescriptionProps, CardDescriptionRef } from '@/components/card/types/card-description'
+import { Text } from '@/components/text'
+
+const CardDescription = React.forwardRef((props, ref) => {
+ const { className, size = 'sm', ...restProps } = props
+
+ return (
+
+ )
+})
+
+CardDescription.displayName = 'CardDescription'
+
+export { CardDescription }
diff --git a/src/components/card/components/card-footer.tsx b/src/components/card/components/card-footer.tsx
new file mode 100644
index 0000000..d806f2d
--- /dev/null
+++ b/src/components/card/components/card-footer.tsx
@@ -0,0 +1,54 @@
+import { cn, getOptionalObject, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_CARD_FOOTER_CHILDREN_CLASSNAME,
+ DEFAULT_CARD_FOOTER_CLASSNAME,
+} from '@/components/card/constants/constants'
+import { CardFooterProps, CardFooterRef } from '@/components/card/types/card-footer'
+
+const CardFooter = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ childrenContainerProps,
+ contentClassName,
+ childrenClassName,
+ startContent,
+ endContent,
+ children,
+ ...restProps
+ } = props
+
+ const {
+ asChild: childrenContainerAsChild,
+ className: childrenContainerClassName,
+ ...restChildrenContainerProps
+ } = getOptionalObject(childrenContainerProps)
+
+ const ContentComponent = polymorphic(asChild, 'div')
+
+ const ChildrenContainerComponent = polymorphic(childrenContainerAsChild, 'span')
+
+ return (
+
+ {startContent}
+
+ {children}
+
+ {endContent}
+
+ )
+})
+
+CardFooter.displayName = 'CardFooter'
+
+export { CardFooter }
diff --git a/src/components/card/components/card-header.tsx b/src/components/card/components/card-header.tsx
new file mode 100644
index 0000000..ef1219d
--- /dev/null
+++ b/src/components/card/components/card-header.tsx
@@ -0,0 +1,58 @@
+import { cn, getOptionalObject, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_CARD_HEADER_CHILDREN_CLASSNAME,
+ DEFAULT_CARD_HEADER_CONTENT_CLASSNAME,
+} from '@/components/card/constants/constants'
+import { CardHeaderProps, CardHeaderRef } from '@/components/card/types/card-header'
+
+const CardHeader = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ childrenContainerProps,
+ contentClassName,
+ childrenClassName,
+ startContent,
+ endContent,
+ children,
+ ...restProps
+ } = props
+
+ const {
+ asChild: childrenContainerAsChild,
+ className: childrenContainerClassName,
+ ...restChildrenContainerProps
+ } = getOptionalObject(childrenContainerProps)
+
+ const ContentComponent = polymorphic(asChild, 'div')
+
+ const ChildrenContainerComponent = polymorphic(childrenContainerAsChild, 'span')
+
+ return (
+
+ {startContent}
+
+ {children}
+
+ {endContent}
+
+ )
+})
+
+CardHeader.displayName = 'CardHeader'
+
+export { CardHeader }
diff --git a/src/components/card/components/card-title.tsx b/src/components/card/components/card-title.tsx
new file mode 100644
index 0000000..8505218
--- /dev/null
+++ b/src/components/card/components/card-title.tsx
@@ -0,0 +1,24 @@
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_CARD_TITLE_CLASSNAME } from '@/components/card/constants/constants'
+import { CardTitleProps, CardTitleRef } from '@/components/card/types/card-title'
+import { Text } from '@/components/text'
+
+const CardTitle = React.forwardRef((props, ref) => {
+ const { className, size = 'md', ...restProps } = props
+
+ return (
+
+ )
+})
+
+CardTitle.displayName = 'CardTitle'
+
+export { CardTitle }
diff --git a/src/components/card/components/card.tsx b/src/components/card/components/card.tsx
new file mode 100644
index 0000000..c59c600
--- /dev/null
+++ b/src/components/card/components/card.tsx
@@ -0,0 +1,26 @@
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { cardClasses } from '@/components/card/classes/card-classes'
+import { CardProps, CardRef } from '@/components/card/types/card'
+
+const Card = React.forwardRef((props, ref) => {
+ const { asChild, className, isBlurred, isPressable, hasShadowOnHover, ...restProps } = props
+
+ const variantClassName = cardClasses({ isBlurred, isPressable, hasShadowOnHover })
+
+ const Component = polymorphic(asChild, 'div')
+
+ return (
+
+ )
+})
+
+Card.displayName = 'Card'
+
+export { Card }
diff --git a/src/components/card/constants/constants.ts b/src/components/card/constants/constants.ts
new file mode 100644
index 0000000..22febfe
--- /dev/null
+++ b/src/components/card/constants/constants.ts
@@ -0,0 +1,26 @@
+const DEFAULT_CARD_HEADER_CONTENT_CLASSNAME =
+ 'render-ui-card-header-content flex items-center gap-4 pt-3 pb-2 px-4'
+
+const DEFAULT_CARD_HEADER_CHILDREN_CLASSNAME = 'render-ui-card-header-children flex grow flex-col'
+
+const DEFAULT_CARD_BODY_CLASSNAME = 'render-ui-card-body py-3 px-4 text-sm'
+
+const DEFAULT_CARD_FOOTER_CLASSNAME =
+ 'render-ui-card-footer flex justify-end items-center gap-2 pt-2 pb-3 px-4'
+
+const DEFAULT_CARD_FOOTER_CHILDREN_CLASSNAME =
+ 'render-ui-card-footer-children flex grow flex-col-reverse gap-2 xs:flex-row xs:justify-end'
+
+const DEFAULT_CARD_TITLE_CLASSNAME = 'render-ui-card-title font-semibold'
+
+const DEFAULT_CARD_DESCRIPTION_CLASSNAME = 'render-ui-card-description text-mode-contrast-accent'
+
+export {
+ DEFAULT_CARD_BODY_CLASSNAME,
+ DEFAULT_CARD_DESCRIPTION_CLASSNAME,
+ DEFAULT_CARD_FOOTER_CHILDREN_CLASSNAME,
+ DEFAULT_CARD_FOOTER_CLASSNAME,
+ DEFAULT_CARD_HEADER_CHILDREN_CLASSNAME,
+ DEFAULT_CARD_HEADER_CONTENT_CLASSNAME,
+ DEFAULT_CARD_TITLE_CLASSNAME,
+}
diff --git a/src/components/card/index.ts b/src/components/card/index.ts
new file mode 100644
index 0000000..a57a88c
--- /dev/null
+++ b/src/components/card/index.ts
@@ -0,0 +1,12 @@
+export * from '@/components/card/components/card'
+export * from '@/components/card/components/card-body'
+export * from '@/components/card/components/card-description'
+export * from '@/components/card/components/card-footer'
+export * from '@/components/card/components/card-header'
+export * from '@/components/card/components/card-title'
+export * from '@/components/card/types/card'
+export * from '@/components/card/types/card-body'
+export * from '@/components/card/types/card-description'
+export * from '@/components/card/types/card-footer'
+export * from '@/components/card/types/card-header'
+export * from '@/components/card/types/card-title'
diff --git a/src/components/card/types/card-body.ts b/src/components/card/types/card-body.ts
new file mode 100644
index 0000000..60dc74e
--- /dev/null
+++ b/src/components/card/types/card-body.ts
@@ -0,0 +1,10 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type CardBodyRef = React.ElementRef<'div'>
+
+type CardBodyProps = Simplify & AsChildProp>
+
+export type { CardBodyProps, CardBodyRef }
diff --git a/src/components/card/types/card-description.ts b/src/components/card/types/card-description.ts
new file mode 100644
index 0000000..216b2cb
--- /dev/null
+++ b/src/components/card/types/card-description.ts
@@ -0,0 +1,7 @@
+import { TextProps, TextRef } from '@/components/text'
+
+type CardDescriptionRef = TextRef
+
+type CardDescriptionProps = TextProps
+
+export type { CardDescriptionProps, CardDescriptionRef }
diff --git a/src/components/card/types/card-footer.ts b/src/components/card/types/card-footer.ts
new file mode 100644
index 0000000..895c884
--- /dev/null
+++ b/src/components/card/types/card-footer.ts
@@ -0,0 +1,20 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type CardFooterRef = React.ElementRef<'div'>
+
+type CardFooterPrimitiveProps = Omit, 'className'>
+
+type CardFooterCustomProps = {
+ startContent?: React.ReactNode
+ endContent?: React.ReactNode
+ childrenClassName?: string
+ contentClassName?: string
+ childrenContainerProps?: Simplify & AsChildProp>
+}
+
+type CardFooterProps = Simplify
+
+export type { CardFooterProps, CardFooterRef }
diff --git a/src/components/card/types/card-header.ts b/src/components/card/types/card-header.ts
new file mode 100644
index 0000000..e8e277e
--- /dev/null
+++ b/src/components/card/types/card-header.ts
@@ -0,0 +1,20 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type CardHeaderRef = React.ElementRef<'div'>
+
+type CardHeaderPrimitiveProps = Omit, 'className'>
+
+type CardHeaderCustomProps = {
+ startContent?: React.ReactNode
+ endContent?: React.ReactNode
+ childrenClassName?: string
+ contentClassName?: string
+ childrenContainerProps?: Simplify & AsChildProp>
+}
+
+type CardHeaderProps = Simplify
+
+export type { CardHeaderProps, CardHeaderRef }
diff --git a/src/components/card/types/card-title.ts b/src/components/card/types/card-title.ts
new file mode 100644
index 0000000..b16ca82
--- /dev/null
+++ b/src/components/card/types/card-title.ts
@@ -0,0 +1,7 @@
+import { TextProps, TextRef } from '@/components/text'
+
+type CardTitleRef = TextRef
+
+type CardTitleProps = TextProps
+
+export type { CardTitleProps, CardTitleRef }
diff --git a/src/components/card/types/card.ts b/src/components/card/types/card.ts
new file mode 100644
index 0000000..6bc5198
--- /dev/null
+++ b/src/components/card/types/card.ts
@@ -0,0 +1,16 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+import { NonNullableVariantProps } from '@/components/_shared/types/variants'
+import { cardClasses } from '@/components/card/classes/card-classes'
+
+type CardRef = React.ElementRef<'div'>
+
+type CardPrimitiveProps = React.ComponentPropsWithoutRef<'div'>
+
+type CardVariantProps = NonNullableVariantProps
+
+type CardProps = Simplify
+
+export type { CardProps, CardRef }
diff --git a/src/components/checkbox/components/checkbox-indicator.tsx b/src/components/checkbox/components/checkbox-indicator.tsx
new file mode 100644
index 0000000..1093199
--- /dev/null
+++ b/src/components/checkbox/components/checkbox-indicator.tsx
@@ -0,0 +1,67 @@
+import { cn, getOptionalObject } from '@renderui/utils'
+import { m } from 'framer-motion'
+import React from 'react'
+
+import {
+ DEFAULT_CHECKBOX_INDICATOR_CLASSNAME,
+ DEFEAULT_MOTION_PROPS,
+} from '@/components/checkbox/constants/constants'
+import {
+ CheckboxIndicatorProps,
+ CheckboxIndicatorRef,
+} from '@/components/checkbox/types/checkbox-indicator'
+import { LazyMotionDomAnimationProvider } from '@/providers'
+
+const CheckboxIndicator = React.forwardRef(
+ (props, ref) => {
+ const {
+ isChecked,
+ hasIconContentWhenUnchecked,
+ className,
+ pathProps,
+ animate,
+ initial = false,
+ fill = 'none',
+ viewBox = '0 0 24 24',
+ ...restProps
+ } = props
+
+ const {
+ strokeLinecap = 'round',
+ strokeLinejoin = 'round',
+ d: dProp = 'M4.5 12.75l6 6 9-13.5',
+ variants,
+ ...restPathprops
+ } = getOptionalObject(pathProps)
+
+ return (
+
+
+
+
+
+ )
+ },
+)
+
+CheckboxIndicator.displayName = 'CheckboxIndicator'
+
+export { CheckboxIndicator }
diff --git a/src/components/checkbox/components/checkbox.tsx b/src/components/checkbox/components/checkbox.tsx
new file mode 100644
index 0000000..2095cd6
--- /dev/null
+++ b/src/components/checkbox/components/checkbox.tsx
@@ -0,0 +1,106 @@
+'use client'
+
+import { useControllableState } from '@renderui/hooks'
+import { chain, cn, cx, functionCallOrValue, getOptionalObject, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button'
+import { CheckboxIndicator } from '@/components/checkbox/components/checkbox-indicator'
+import {
+ DEFAULT_CHECKBOX_CLASSNAME,
+ DEFAULT_CHECKBOX_HIDDEN_INPUT_CLASSNAME,
+} from '@/components/checkbox/constants/constants'
+import { CheckboxProps, CheckboxRef } from '@/components/checkbox/types/checkbox'
+import { VisuallyHidden } from '@/components/visually-hidden'
+
+const Checkbox = React.forwardRef((props, ref) => {
+ const {
+ inputRef,
+ inputProps,
+ name,
+ className,
+ defaultChecked,
+ isChecked: checkedProp,
+ isDisabled,
+ isInvalid,
+ isReadOnly,
+ isRequired,
+ startContent,
+ endContent,
+ children,
+ onCheckedChange,
+ onPress,
+ hasRipple = false,
+ hasIconContentWhenUnchecked = true,
+ ...restProps
+ } = props
+
+ const [checked, setChecked] = useControllableState({
+ prop: checkedProp,
+ defaultProp: defaultChecked,
+ onChange: onCheckedChange,
+ })
+
+ const {
+ asChild,
+ name: inputName,
+ value: inputValue,
+ className: inputClassName,
+ checked: inputChecked,
+ tabIndex = -1,
+ onChange,
+ ...restInputProps
+ } = getOptionalObject(inputProps)
+
+ const InputComponent = polymorphic(asChild, 'input')
+
+ return (
+ setChecked((previousChecked) => !previousChecked))}
+ {...restProps}
+ >
+ {functionCallOrValue(startContent, checked)}
+ {functionCallOrValue(children, checked)}
+
+
+ ) =>
+ setChecked(event.target.checked),
+ )}
+ {...restInputProps}
+ />
+
+ {functionCallOrValue(endContent, checked)}
+
+ )
+})
+
+Checkbox.displayName = 'Checkbox'
+
+export { Checkbox }
diff --git a/src/components/checkbox/constants/constants.ts b/src/components/checkbox/constants/constants.ts
new file mode 100644
index 0000000..3e97143
--- /dev/null
+++ b/src/components/checkbox/constants/constants.ts
@@ -0,0 +1,39 @@
+const DEFAULT_CHECKBOX_CLASSNAME =
+ 'render-ui-checkbox p-0.5 bg-transparent rounded-md min-w-0 h-fit w-fit aspect-square shrink-0 text-white data-[state=checked]:bg-primary'
+
+const DEFAULT_CHECKBOX_INDICATOR_CLASSNAME =
+ 'render-ui-checkbox-indicator h-[0.85rem] w-[0.85rem] stroke-white stroke-[2.5]'
+
+const DEFAULT_CHECKBOX_HIDDEN_INPUT_CLASSNAME = 'render-ui-checkbox-hidden-input'
+
+const DEFEAULT_MOTION_PROPS = {
+ checked: {
+ pathLength: 1,
+ opacity: 1,
+ transition: {
+ duration: 0.275,
+ delay: 0,
+ },
+ },
+ unchecked: {
+ pathLength: 0,
+ opacity: 0,
+ transition: {
+ duration: 0,
+ delay: 0,
+ },
+ },
+} as const
+
+const CHECKBOX_BUTTON_ROLE_TYPE_CONFIG = { role: 'checkbox', type: 'button' } as const
+
+const CHECKBOX_INPUT_TYPE_CONFIG = { type: 'checkbox' } as const
+
+export {
+ CHECKBOX_BUTTON_ROLE_TYPE_CONFIG,
+ CHECKBOX_INPUT_TYPE_CONFIG,
+ DEFAULT_CHECKBOX_CLASSNAME,
+ DEFAULT_CHECKBOX_HIDDEN_INPUT_CLASSNAME,
+ DEFAULT_CHECKBOX_INDICATOR_CLASSNAME,
+ DEFEAULT_MOTION_PROPS,
+}
diff --git a/src/components/checkbox/index.ts b/src/components/checkbox/index.ts
new file mode 100644
index 0000000..13dd9f3
--- /dev/null
+++ b/src/components/checkbox/index.ts
@@ -0,0 +1,4 @@
+export * from '@/components/checkbox/components/checkbox'
+export * from '@/components/checkbox/components/checkbox-indicator'
+export * from '@/components/checkbox/types/checkbox'
+export * from '@/components/checkbox/types/checkbox-indicator'
diff --git a/src/components/checkbox/types/checkbox-indicator.ts b/src/components/checkbox/types/checkbox-indicator.ts
new file mode 100644
index 0000000..a35605d
--- /dev/null
+++ b/src/components/checkbox/types/checkbox-indicator.ts
@@ -0,0 +1,22 @@
+import { Simplify } from '@renderui/types'
+import { m } from 'framer-motion'
+import React from 'react'
+
+type CheckboxIndicatorPrimitive = typeof m.svg
+
+type CheckboxIndicatorRef = React.ElementRef
+
+// type CheckboxIndicatorPrimitiveProps = React.ComponentPropsWithoutRef
+type CheckboxIndicatorPrimitiveProps = any
+
+type CheckboxIndicatorRenderUIProps = {
+ isChecked?: boolean
+ hasIconContentWhenUnchecked?: boolean
+ pathProps?: React.ComponentPropsWithoutRef
+}
+
+type CheckboxIndicatorProps = Simplify<
+ CheckboxIndicatorPrimitiveProps & CheckboxIndicatorRenderUIProps
+>
+
+export type { CheckboxIndicatorProps, CheckboxIndicatorRef }
diff --git a/src/components/checkbox/types/checkbox.ts b/src/components/checkbox/types/checkbox.ts
new file mode 100644
index 0000000..3906d73
--- /dev/null
+++ b/src/components/checkbox/types/checkbox.ts
@@ -0,0 +1,33 @@
+import { Primitive as primitive } from '@radix-ui/react-primitive'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+import { ButtonProps, ButtonRef } from '@/components/button'
+
+type CheckboxRef = ButtonRef
+
+type CheckboxButtonProps = Omit<
+ ButtonProps,
+ 'children' | 'disabled' | 'readonly' | 'required' | 'startContent' | 'children' | 'endContent'
+>
+
+type CheckboxCustomProps = {
+ children?: React.ReactNode | ((checked: boolean) => React.ReactNode)
+ startContent?: React.ReactNode | ((checked: boolean) => React.ReactNode)
+ endContent?: React.ReactNode | ((checked: boolean) => React.ReactNode)
+ inputRef?: React.RefObject
+ inputProps?: Simplify & AsChildProp>
+ name?: string
+ isChecked?: boolean
+ isReadOnly?: boolean
+ isInvalid?: boolean
+ isRequired?: boolean
+ defaultChecked?: boolean
+ onCheckedChange?: React.Dispatch>
+ hasIconContentWhenUnchecked?: boolean
+}
+
+type CheckboxProps = Simplify
+
+export type { CheckboxProps, CheckboxRef }
diff --git a/src/components/collapsible/components/collapsible-content.tsx b/src/components/collapsible/components/collapsible-content.tsx
new file mode 100644
index 0000000..be68c0e
--- /dev/null
+++ b/src/components/collapsible/components/collapsible-content.tsx
@@ -0,0 +1,27 @@
+import { CollapsibleContent as CollapsibleContentPrimitive } from '@radix-ui/react-collapsible'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import {
+ CollapsibleContentProps,
+ CollapsibleContentRef,
+} from '@/components/collapsible/types/collapsible-content'
+
+const CollapsibleContent = React.forwardRef(
+ (props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+ },
+)
+
+CollapsibleContent.displayName = 'CollapsibleContent'
+
+export { CollapsibleContent }
diff --git a/src/components/collapsible/components/collapsible-trigger.tsx b/src/components/collapsible/components/collapsible-trigger.tsx
new file mode 100644
index 0000000..5f0f6a7
--- /dev/null
+++ b/src/components/collapsible/components/collapsible-trigger.tsx
@@ -0,0 +1,32 @@
+import { CollapsibleTrigger as CollapsibleTriggerPrimitive } from '@radix-ui/react-collapsible'
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button'
+import {
+ CollapsibleTriggerProps,
+ CollapsibleTriggerRef,
+} from '@/components/collapsible/types/collapsible-trigger'
+
+const CollapsibleTrigger = React.forwardRef(
+ (props, ref) => {
+ const { className, variant = 'solid', color = 'mode-accent' } = props
+
+ return (
+
+
+
+ )
+ },
+)
+
+CollapsibleTrigger.displayName = 'CollapsibleTrigger'
+
+export { CollapsibleTrigger }
diff --git a/src/components/collapsible/components/collapsible.tsx b/src/components/collapsible/components/collapsible.tsx
new file mode 100644
index 0000000..ae292be
--- /dev/null
+++ b/src/components/collapsible/components/collapsible.tsx
@@ -0,0 +1,7 @@
+import { Collapsible as CollapsiblePrimitive } from '@radix-ui/react-collapsible'
+
+const Collapsible = CollapsiblePrimitive
+
+Collapsible.displayName = 'Collapsible'
+
+export { Collapsible }
diff --git a/src/components/collapsible/index.ts b/src/components/collapsible/index.ts
new file mode 100644
index 0000000..28f8964
--- /dev/null
+++ b/src/components/collapsible/index.ts
@@ -0,0 +1,6 @@
+export * from '@/components/collapsible/components/collapsible'
+export * from '@/components/collapsible/components/collapsible-content'
+export * from '@/components/collapsible/components/collapsible-trigger'
+export * from '@/components/collapsible/types/collapsible'
+export * from '@/components/collapsible/types/collapsible-content'
+export * from '@/components/collapsible/types/collapsible-trigger'
diff --git a/src/components/collapsible/types/collapsible-content.ts b/src/components/collapsible/types/collapsible-content.ts
new file mode 100644
index 0000000..160c3b1
--- /dev/null
+++ b/src/components/collapsible/types/collapsible-content.ts
@@ -0,0 +1,9 @@
+import { CollapsibleContent as CollapsibleContentPrimitive } from '@radix-ui/react-collapsible'
+
+type CollapsibleContentPrimitiveType = typeof CollapsibleContentPrimitive
+
+type CollapsibleContentRef = React.ElementRef
+
+type CollapsibleContentProps = React.ComponentPropsWithoutRef
+
+export type { CollapsibleContentProps, CollapsibleContentRef }
diff --git a/src/components/collapsible/types/collapsible-trigger.ts b/src/components/collapsible/types/collapsible-trigger.ts
new file mode 100644
index 0000000..40f93fa
--- /dev/null
+++ b/src/components/collapsible/types/collapsible-trigger.ts
@@ -0,0 +1,7 @@
+import { ButtonProps, ButtonRef } from '@/components/button'
+
+type CollapsibleTriggerRef = ButtonRef
+
+type CollapsibleTriggerProps = ButtonProps
+
+export type { CollapsibleTriggerProps, CollapsibleTriggerRef }
diff --git a/src/components/collapsible/types/collapsible.ts b/src/components/collapsible/types/collapsible.ts
new file mode 100644
index 0000000..1bca85e
--- /dev/null
+++ b/src/components/collapsible/types/collapsible.ts
@@ -0,0 +1,9 @@
+import { Collapsible as CollapsiblePrimitive } from '@radix-ui/react-collapsible'
+
+type CollapsiblePrimitiveType = typeof CollapsiblePrimitive
+
+type CollapsibleRef = React.ElementRef
+
+type CollapsibleProps = React.ComponentPropsWithoutRef
+
+export type { CollapsibleProps, CollapsibleRef }
diff --git a/src/components/combobox/components/combobox-content.tsx b/src/components/combobox/components/combobox-content.tsx
new file mode 100644
index 0000000..84380e9
--- /dev/null
+++ b/src/components/combobox/components/combobox-content.tsx
@@ -0,0 +1,150 @@
+'use client'
+
+import { cx,getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import { ComboboxInput } from '@/components/combobox/components/combobox-input'
+import {
+ COMBOBOX_INPUT_CONTAINER_CLASSNAME,
+ DEFAULT_COMBOBOX_COMMAND_CLASSNAME,
+ DEFAULT_COMBOBOX_COMMAND_GROUP_CLASSNAME,
+ DEFAULT_COMBOBOX_CONTENT_CLASSNAME,
+ DEFAULT_COMBOBOX_INPUT_CLASSNAME,
+ DEFAULT_COMBOBOX_SCROLL_AREA_CLASSNAME,
+ DEFAULT_COMBOBOX_SCROLL_AREA_SCROLLBAR_CLASSNAME,
+ SELECT_INPUT_CONTAINER_CLASSNAME,
+} from '@/components/combobox/constants/constants'
+import { useComboboxContext } from '@/components/combobox/contexts/combobox-context'
+import { useLazyScrollAreaComponent } from '@/components/combobox/hooks/use-lazy-scroll-area-component'
+import {
+ ComboboxContentProps,
+ ComboboxContentRef,
+} from '@/components/combobox/types/combobox-content'
+import { Command, CommandEmpty, CommandGroup } from '@/components/command'
+import { PopoverContent } from '@/components/popover'
+import { ScrollAreaScrollbarProps } from '@/components/scroll-area/types/scroll-area-scrollbar'
+
+const ComboboxContent = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ placeholder,
+ emptyContent,
+ commandProps,
+ commandInputProps,
+ commandEmptyProps,
+ commandGroupProps,
+ scrollAreaProps,
+ align = 'start',
+ side = 'bottom',
+ hasScroll = false,
+ hasEmptyContent = true,
+ hasTriggerMinWidth = true,
+ ...restProps
+ } = props
+
+ const { type, focusValue, setFocusValue } = useComboboxContext()
+
+ const {
+ loop = true,
+ className: commandClassName,
+ ...restCommandProps
+ } = getOptionalObject(commandProps)
+
+ const {
+ placeholder: commandInputPlaceholder,
+ className: commandInputClassName,
+ ...restCommandInputProps
+ } = getOptionalObject(commandInputProps)
+
+ const { children: commandEmptyContent, ...restCommandEmptyProps } =
+ getOptionalObject(commandEmptyProps)
+
+ const { className: commandGroupClassName, ...restCommandGroupClassName } =
+ getOptionalObject(commandGroupProps)
+
+ const {
+ className: scrollAreaClassName,
+ scrollbarProps,
+ ...restScrollAreaProps
+ } = getOptionalObject(scrollAreaProps)
+
+ const { className: scrollbarClassName, ...restScrollbarProps } = getOptionalObject(scrollbarProps)
+
+ const ScrollAreaComponent = useLazyScrollAreaComponent(hasScroll)
+
+ return (
+
+
+
+ {hasEmptyContent && (commandEmptyContent || emptyContent) ? (
+
+ {commandEmptyContent ?? emptyContent}
+
+ ) : null}
+ {hasScroll && ScrollAreaComponent ? (
+
+
+ {children}
+
+
+ ) : (
+
+ {children}
+
+ )}
+
+
+ )
+})
+
+ComboboxContent.displayName = 'ComboboxContent'
+
+export { ComboboxContent }
diff --git a/src/components/combobox/components/combobox-input.tsx b/src/components/combobox/components/combobox-input.tsx
new file mode 100644
index 0000000..cdff782
--- /dev/null
+++ b/src/components/combobox/components/combobox-input.tsx
@@ -0,0 +1,27 @@
+'use client'
+
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { ComboboxInputProps, ComboboxInputRef } from '@/components/combobox/types/combobox-input'
+import { CommandInput } from '@/components/command'
+import { COMMAND_INPUT_CLASSNAME } from '@/components/command/constants/constants'
+
+const ComboboxInput = React.forwardRef((props, ref) => {
+ const { className, containerProps, iconProps, ...restProps } = props
+
+ return (
+
+ )
+})
+
+ComboboxInput.displayName = 'ComboboxInput'
+
+export { ComboboxInput }
diff --git a/src/components/combobox/components/combobox-item.tsx b/src/components/combobox/components/combobox-item.tsx
new file mode 100644
index 0000000..bfea6b9
--- /dev/null
+++ b/src/components/combobox/components/combobox-item.tsx
@@ -0,0 +1,88 @@
+'use client'
+
+import { CheckIcon } from '@radix-ui/react-icons'
+import { chain, cn, functionCallOrValue, getNestedChildrenTextContent } from '@renderui/utils'
+import React from 'react'
+
+import {
+ COMBOBOX_ITEM_CHECK_ICON_CHECKED_CLASSNAME,
+ DEFAULT_COMBOBOX_ITEM_CHECK_ICON_CLASSNAME,
+ DEFAULT_COMBOBOX_ITEM_CLASSNAME,
+} from '@/components/combobox/constants/constants'
+import { useComboboxContext } from '@/components/combobox/contexts/combobox-context'
+import { ComboboxItemProps, ComboboxItemRef } from '@/components/combobox/types/combobox-item'
+import { CommandItem } from '@/components/command'
+
+// @TODO waiting for cmdk fix https://github.com/pacocoursey/cmdk/issues/150 to be implemented
+
+const ComboboxItem = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ startContent,
+ endContent,
+ value,
+ onSelect,
+ role = 'option',
+ ...restProps
+ } = props
+
+ const {
+ value: rootValue,
+ open,
+ label,
+ hasCheckIcon,
+ setOpen,
+ setValue,
+ setLabel,
+ setFocusValue,
+ } = useComboboxContext()
+
+ const childrenTextContent = React.useMemo(
+ () => getNestedChildrenTextContent(children),
+ [children],
+ )
+
+ const handleSelect = () => {
+ const isUnselect = value === rootValue
+
+ setValue(isUnselect ? '' : value)
+ setLabel(isUnselect ? '' : childrenTextContent)
+ setFocusValue(isUnselect ? '' : childrenTextContent)
+ setOpen(false)
+ }
+
+ const isChecked = value === rootValue
+
+ return (
+
+ {functionCallOrValue(startContent, isChecked)}
+ {functionCallOrValue(children, isChecked)}
+ {hasCheckIcon ? (
+
+ ) : null}
+ {functionCallOrValue(endContent, isChecked)}
+
+ )
+})
+
+ComboboxItem.displayName = 'ComboboxItem'
+
+export { ComboboxItem }
diff --git a/src/components/combobox/components/combobox-trigger.tsx b/src/components/combobox/components/combobox-trigger.tsx
new file mode 100644
index 0000000..ab5aab5
--- /dev/null
+++ b/src/components/combobox/components/combobox-trigger.tsx
@@ -0,0 +1,122 @@
+'use client'
+
+import { CaretSortIcon } from '@radix-ui/react-icons'
+import { useMergedRef } from '@renderui/hooks'
+import { chain, cn, cx, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_COMBOBOX_TRIGGER_CLASSNAME,
+ DEFAULT_COMBOBOX_TRIGGER_ICON_CLASSNAME,
+} from '@/components/combobox/constants/constants'
+import { useComboboxContext } from '@/components/combobox/contexts/combobox-context'
+import {
+ ComboboxTriggerProps,
+ ComboboxTriggerRef,
+} from '@/components/combobox/types/combobox-trigger'
+import { getHandleKeyDownCapture } from '@/components/combobox/utils/get-handle-keydown-capture'
+import { PopoverTrigger } from '@/components/popover'
+
+const ComboboxTrigger = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ placeholder,
+ iconProps,
+ role = 'combobox',
+ 'aria-haspopup': ariaHasPopup = 'listbox',
+ variant = 'solid',
+ color = 'mode-accent',
+ hasTruncatedText = false,
+ hasDefaultPressedStyles = false,
+ hasRipple = false,
+ onKeyDownCapture,
+ ...restProps
+ } = props
+
+ const {
+ label,
+ value,
+ open,
+ triggerRef,
+ isDisabled,
+ isInvalid,
+ isReadonly,
+ isRequired,
+ setOpen,
+ setFocusValue,
+ } = useComboboxContext()
+
+ const mergedRefCallaback = useMergedRef([triggerRef, ref])
+
+ const timeoutIdRef = React.useRef(null)
+
+ React.useEffect(
+ () => () => {
+ if (!timeoutIdRef.current) return
+
+ clearTimeout(timeoutIdRef.current)
+ },
+ [],
+ )
+
+ const { className: iconClassName } = getOptionalObject(iconProps)
+
+ const content = (
+ <>
+ {label || placeholder}
+ {children}
+ >
+ )
+
+ return (
+
+ {hasTruncatedText ? (
+ {content}
+ ) : (
+ content
+ )}
+
+
+ )
+})
+
+ComboboxTrigger.displayName = 'ComboboxTrigger'
+
+export { ComboboxTrigger }
diff --git a/src/components/combobox/components/combobox.tsx b/src/components/combobox/components/combobox.tsx
new file mode 100644
index 0000000..6ed1405
--- /dev/null
+++ b/src/components/combobox/components/combobox.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import { NEGATIVE_ONE } from '@renderui/constants'
+import { useControllableState } from '@renderui/hooks'
+import React, { useState } from 'react'
+import { chain } from 'react-aria'
+
+import { ComboboxProvider } from '@/components/combobox/contexts/combobox-context'
+import { ComboboxProps } from '@/components/combobox/types/combobox'
+import { Popover } from '@/components/popover'
+import { VisuallyHidden } from '@/components/visually-hidden'
+
+const Combobox = (props: ComboboxProps) => {
+ const {
+ open: openProp,
+ value: valueProp,
+ name,
+ children,
+ inputProps,
+ triggerRef,
+ isDisabled,
+ isInvalid,
+ isReadonly,
+ isRequired,
+ onOpenChange,
+ onValueChange,
+ defaultValue = '',
+ defaultOpen = false,
+ hasCheckIcon = true,
+ type = 'combobox',
+ ...restProps
+ } = props
+
+ const [open, setOpen] = useControllableState({
+ defaultProp: defaultOpen,
+ prop: openProp,
+ onChange: onOpenChange,
+ })
+
+ const [value, setValue] = useControllableState({
+ defaultProp: defaultValue,
+ prop: valueProp,
+ onChange: onValueChange,
+ })
+
+ const [focusValue, setFocusValue] = useState('')
+
+ const [label, setLabel] = React.useState('')
+
+ const handleRefocusToActiveItem = () => {
+ setFocusValue(label)
+ }
+
+ return (
+
+
+ {children}
+
+
+ setValue(event.target.value)}
+ {...inputProps}
+ />
+
+
+ )
+}
+
+export { Combobox }
diff --git a/src/components/combobox/constants/constants.ts b/src/components/combobox/constants/constants.ts
new file mode 100644
index 0000000..6426cde
--- /dev/null
+++ b/src/components/combobox/constants/constants.ts
@@ -0,0 +1,57 @@
+const DEFAULT_COMBOBOX_TRIGGER_CLASSNAME =
+ 'render-ui-combobox-trigger group justify-between transition-[background-color,box-shadow] data-[empty=true]:text-mode-foreground/50 aria-[expanded=true]:before:ring-ring-color aria-[expanded=true]:ring-ring-color aria-[expanded=true]:ring-[1px] ring-offset-0 data-[focus-visible]:ring-offset-0 data-[hover=true]:ring-[1px] data-[expanded=true]:ring-[2px] data-[expanded=true]:data-[hover=true]:ring-[2px]'
+
+const DEFAULT_COMBOBOX_TRIGGER_ICON_CLASSNAME =
+ 'render-ui-combobox-trigger-icon inline-block h-4 w-4 shrink-0 opacity-50 text-mode-foreground transition-[transform] duration-fast group-aria-expanded:rotate-[-90deg]'
+
+const DEFAULT_COMBOBOX_INPUT_CLASSNAME = 'render-ui-combobox-content h-9'
+
+const DEFAULT_COMBOBOX_ITEM_CLASSNAME = 'render-ui-combobox-item flex items-center gap-2 break-all'
+
+const DEFAULT_COMBOBOX_ITEM_CHECK_ICON_CLASSNAME =
+ 'render-ui-combobox-item-check-icon ml-auto h-4 w-4 shrink-0 opacity-0'
+
+const COMBOBOX_ITEM_CHECK_ICON_CHECKED_CLASSNAME = 'opacity-100'
+
+const DEFAULT_INPUT_CONTAINER_CLASSNAME = 'flex items-center border-b px-3'
+
+const COMBOBOX_INPUT_CONTAINER_CLASSNAME = 'render-ui-combobox-input-container'
+
+const SELECT_INPUT_CONTAINER_CLASSNAME = 'render-ui-select-input-container sr-only'
+
+const ITEM_CLASSNAME = '.render-ui-combobox-item'
+
+const ACTIVE_ITEM_CLASSNAME = '.render-ui-combobox-item[data-selected="true"]'
+
+const ALLOWED_SELECT_KEYS = ['ArrowUp', 'ArrowDown', 'Enter', 'Escape'] as const
+
+const DEFAULT_COMBOBOX_CONTENT_CLASSNAME = 'bg-mode p-0'
+
+const DEFAULT_COMBOBOX_COMMAND_CLASSNAME = 'bg-transparent'
+
+const DEFAULT_COMBOBOX_COMMAND_GROUP_CLASSNAME = 'bg-transparent'
+
+const DEFAULT_COMBOBOX_SCROLL_AREA_CLASSNAME = 'render-ui-combobox-scroll-area w-[7px]'
+
+const DEFAULT_COMBOBOX_SCROLL_AREA_SCROLLBAR_CLASSNAME =
+ 'render-ui-combobox-scroll-area-scrollbar max-h-80 bg-transparent sm:max-h-80'
+
+export {
+ ACTIVE_ITEM_CLASSNAME,
+ ALLOWED_SELECT_KEYS,
+ COMBOBOX_INPUT_CONTAINER_CLASSNAME,
+ COMBOBOX_ITEM_CHECK_ICON_CHECKED_CLASSNAME,
+ DEFAULT_COMBOBOX_COMMAND_CLASSNAME,
+ DEFAULT_COMBOBOX_COMMAND_GROUP_CLASSNAME,
+ DEFAULT_COMBOBOX_CONTENT_CLASSNAME,
+ DEFAULT_COMBOBOX_INPUT_CLASSNAME,
+ DEFAULT_COMBOBOX_ITEM_CHECK_ICON_CLASSNAME,
+ DEFAULT_COMBOBOX_ITEM_CLASSNAME,
+ DEFAULT_COMBOBOX_SCROLL_AREA_CLASSNAME,
+ DEFAULT_COMBOBOX_SCROLL_AREA_SCROLLBAR_CLASSNAME,
+ DEFAULT_COMBOBOX_TRIGGER_CLASSNAME,
+ DEFAULT_COMBOBOX_TRIGGER_ICON_CLASSNAME,
+ DEFAULT_INPUT_CONTAINER_CLASSNAME,
+ ITEM_CLASSNAME,
+ SELECT_INPUT_CONTAINER_CLASSNAME,
+}
diff --git a/src/components/combobox/contexts/combobox-context.ts b/src/components/combobox/contexts/combobox-context.ts
new file mode 100644
index 0000000..2e3fa57
--- /dev/null
+++ b/src/components/combobox/contexts/combobox-context.ts
@@ -0,0 +1,12 @@
+import { initializeContext } from '@renderui/utils'
+
+import { ComboboxContext } from '@/components/combobox/types/combobox-context'
+
+const [ComboboxProvider, useComboboxContext] = initializeContext({
+ errorMessage: 'Components using combobox context must be wrapped in a .',
+ providerName: 'ComboboxProvider',
+ hookName: 'useComboboxContext',
+ name: 'ComboboxContext',
+})
+
+export { ComboboxProvider, useComboboxContext }
diff --git a/src/components/combobox/hooks/use-lazy-scroll-area-component.ts b/src/components/combobox/hooks/use-lazy-scroll-area-component.ts
new file mode 100644
index 0000000..9b76728
--- /dev/null
+++ b/src/components/combobox/hooks/use-lazy-scroll-area-component.ts
@@ -0,0 +1,22 @@
+/* eslint-disable react/hook-use-state */
+import React from 'react'
+
+import { ComboboxContentProps } from '@/components/combobox/types/combobox-content'
+
+function useLazyScrollAreaComponent(hasScroll: ComboboxContentProps['hasScroll']) {
+ const [ScrollAreaComponent, setScrollComponent] = React.useState(null)
+
+ React.useEffect(() => {
+ const loadComponent = async () => {
+ const { ScrollArea } = await import('@/components/scroll-area')
+
+ setScrollComponent(ScrollArea)
+ }
+
+ loadComponent()
+ }, [hasScroll])
+
+ return ScrollAreaComponent
+}
+
+export { useLazyScrollAreaComponent }
diff --git a/src/components/combobox/index.ts b/src/components/combobox/index.ts
new file mode 100644
index 0000000..834d1e9
--- /dev/null
+++ b/src/components/combobox/index.ts
@@ -0,0 +1,10 @@
+export * from '@/components/combobox/components/combobox'
+export * from '@/components/combobox/components/combobox-content'
+export * from '@/components/combobox/components/combobox-input'
+export * from '@/components/combobox/components/combobox-item'
+export * from '@/components/combobox/components/combobox-trigger'
+export * from '@/components/combobox/types/combobox'
+export * from '@/components/combobox/types/combobox-content'
+export * from '@/components/combobox/types/combobox-input'
+export * from '@/components/combobox/types/combobox-item'
+export * from '@/components/combobox/types/combobox-trigger'
\ No newline at end of file
diff --git a/src/components/combobox/types/combobox-content.ts b/src/components/combobox/types/combobox-content.ts
new file mode 100644
index 0000000..5a79154
--- /dev/null
+++ b/src/components/combobox/types/combobox-content.ts
@@ -0,0 +1,28 @@
+import { Simplify } from '@renderui/types'
+import { Command, CommandEmpty, CommandGroup, CommandInput } from 'cmdk'
+import React from 'react'
+
+import { PopoverContentProps, PopoverContentRef } from '@/components/popover'
+import { ScrollArea } from '@/components/scroll-area'
+
+type ComboboxContentRef = PopoverContentRef
+
+type ComboboxContentPopoverContentProps = PopoverContentProps
+
+type ComboboxContentCustomProps = {
+ placeholder?: string
+ hasEmptyContent?: boolean
+ hasScroll?: boolean
+ emptyContent?: React.ReactNode
+ commandProps?: React.ComponentPropsWithoutRef
+ commandInputProps?: React.ComponentPropsWithoutRef
+ commandEmptyProps?: React.ComponentPropsWithoutRef
+ commandGroupProps?: React.ComponentPropsWithoutRef
+ scrollAreaProps?: React.ComponentPropsWithoutRef
+}
+
+type ComboboxContentProps = Simplify<
+ ComboboxContentPopoverContentProps & ComboboxContentCustomProps
+>
+
+export type { ComboboxContentProps, ComboboxContentRef }
diff --git a/src/components/combobox/types/combobox-context.ts b/src/components/combobox/types/combobox-context.ts
new file mode 100644
index 0000000..08e3e0a
--- /dev/null
+++ b/src/components/combobox/types/combobox-context.ts
@@ -0,0 +1,24 @@
+import { RefCallback } from 'react'
+
+import { ButtonRef } from '@/components/button'
+import { ComboboxProps } from '@/components/combobox/types/combobox'
+
+type ComboboxContext = {
+ type: NonNullable
+ open: ComboboxProps['open']
+ value: ComboboxProps['value']
+ focusValue: ComboboxProps['value']
+ label: React.ReactNode
+ triggerRef: React.RefObject | RefCallback | undefined
+ hasCheckIcon: boolean
+ isDisabled: boolean | undefined
+ isInvalid: boolean | undefined
+ isReadonly: boolean | undefined
+ isRequired: boolean | undefined
+ setOpen: React.Dispatch>
+ setValue: React.Dispatch>
+ setFocusValue: React.Dispatch>
+ setLabel: React.Dispatch>
+}
+
+export type { ComboboxContext }
diff --git a/src/components/combobox/types/combobox-input.ts b/src/components/combobox/types/combobox-input.ts
new file mode 100644
index 0000000..0f01c86
--- /dev/null
+++ b/src/components/combobox/types/combobox-input.ts
@@ -0,0 +1,20 @@
+import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
+import { Primitive as primitive } from '@radix-ui/react-primitive'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { CommandInputProps, CommandInputRef } from '@/components/command'
+
+
+type ComboboxInputRef = CommandInputRef
+
+type ComboboxInputCommandInputProps = CommandInputProps
+
+type ComboboxInputCustomProps = {
+ containerProps?: React.ComponentPropsWithoutRef
+ iconProps?: React.ComponentPropsWithoutRef
+}
+
+type ComboboxInputProps = Simplify
+
+export type { ComboboxInputProps, ComboboxInputRef }
diff --git a/src/components/combobox/types/combobox-item.ts b/src/components/combobox/types/combobox-item.ts
new file mode 100644
index 0000000..639a47e
--- /dev/null
+++ b/src/components/combobox/types/combobox-item.ts
@@ -0,0 +1,18 @@
+import { Simplify } from '@renderui/types'
+
+import { CommandItemProps, CommandItemRef } from '@/components/command'
+
+type ComboboxItemRef = CommandItemRef
+
+type ComboboxItemCommandItemProps = Omit
+
+type ComboboxItemCustomProps = {
+ value: string
+ children: React.ReactNode
+ startContent?: React.ReactNode
+ endContent?: React.ReactNode
+}
+
+type ComboboxItemProps = Simplify
+
+export type { ComboboxItemProps, ComboboxItemRef }
diff --git a/src/components/combobox/types/combobox-trigger.ts b/src/components/combobox/types/combobox-trigger.ts
new file mode 100644
index 0000000..b31752d
--- /dev/null
+++ b/src/components/combobox/types/combobox-trigger.ts
@@ -0,0 +1,19 @@
+import { CaretSortIcon } from '@radix-ui/react-icons'
+import { Simplify } from '@renderui/types'
+
+import { ButtonProps, ButtonRef } from '@/components/button'
+
+type ComboboxTriggerRef = ButtonRef
+
+type ComboboxTriggerButtonProps = Omit
+
+type ComboboxTriggerCustomProps = {
+ placeholder?: string
+ hasTruncatedText?: boolean
+ children?: React.ReactNode
+ iconProps?: React.ComponentPropsWithoutRef
+}
+
+type ComboboxTriggerProps = Simplify
+
+export type { ComboboxTriggerProps, ComboboxTriggerRef }
diff --git a/src/components/combobox/types/combobox.ts b/src/components/combobox/types/combobox.ts
new file mode 100644
index 0000000..ef036ad
--- /dev/null
+++ b/src/components/combobox/types/combobox.ts
@@ -0,0 +1,25 @@
+import { Simplify } from '@renderui/types'
+
+import { ButtonRef } from '@/components/button'
+import { PopoverProps } from '@/components/popover'
+
+type ComboboxPopoverProps = PopoverProps
+
+type ComboboxCustomProps = {
+ type?: 'select' | 'combobox'
+ name?: string
+ value?: string
+ defaultValue?: string
+ inputProps?: React.ComponentPropsWithoutRef<'input'>
+ triggerRef?: React.RefObject
+ hasCheckIcon?: boolean
+ isDisabled?: boolean | undefined
+ isInvalid?: boolean | undefined
+ isReadonly?: boolean | undefined
+ isRequired?: boolean | undefined
+ onValueChange?: React.Dispatch>
+}
+
+type ComboboxProps = Simplify
+
+export type { ComboboxProps }
diff --git a/src/components/combobox/utils/get-handle-input-keydown-capture.ts b/src/components/combobox/utils/get-handle-input-keydown-capture.ts
new file mode 100644
index 0000000..3cc3c24
--- /dev/null
+++ b/src/components/combobox/utils/get-handle-input-keydown-capture.ts
@@ -0,0 +1,43 @@
+import React from 'react'
+
+import { ACTIVE_ITEM_CLASSNAME } from '@/components/combobox/constants/constants'
+
+type GetHandleInputKeyDownCaptureArgs = {
+ variant: 'select' | 'combobox'
+ setOpen: React.Dispatch>
+ setValue: React.Dispatch>
+ setLabel: React.Dispatch>
+}
+
+function getHandleInputKeyDownCapture(args: GetHandleInputKeyDownCaptureArgs) {
+ const { variant, setOpen, setLabel, setValue } = args
+
+ // If the variant is of type 'combobox', keep default functionality, space press adds space string to input
+ if (variant === 'combobox') return undefined
+
+ // if the variant is of type 'select', prevent default functionality, space press selects the active item
+ return (event: React.KeyboardEvent) => {
+ const { key } = event
+
+ // if (!ALLOWED_SELECT_KEYS.includes(key)) {
+ // event.preventDefault()
+ // }
+
+ if (key === ' ') {
+ event.preventDefault()
+
+ const activeItem = document.querySelector(ACTIVE_ITEM_CLASSNAME)
+
+ if (!activeItem || !(activeItem instanceof HTMLElement)) return
+
+ const activeItemValue = activeItem.dataset.inputValue ?? ''
+ const activeItemLabel = activeItem.textContent ?? ''
+
+ setValue(activeItemValue)
+ setLabel(activeItemLabel)
+ setOpen(false)
+ }
+ }
+}
+
+export { getHandleInputKeyDownCapture }
diff --git a/src/components/combobox/utils/get-handle-keydown-capture.ts b/src/components/combobox/utils/get-handle-keydown-capture.ts
new file mode 100644
index 0000000..9037bb6
--- /dev/null
+++ b/src/components/combobox/utils/get-handle-keydown-capture.ts
@@ -0,0 +1,47 @@
+import { ZERO } from '@renderui/constants'
+
+import { ACTIVE_ITEM_CLASSNAME } from '@/components/combobox/constants/constants'
+import { getNextOrPreviousItem } from '@/components/combobox/utils/get-next-or-previous-item'
+
+type GetHandleKeyDownCaptureArgs = {
+ open: boolean | undefined
+ value: string | undefined
+ timeoutIdRef: React.MutableRefObject
+ setOpen: (open: boolean) => void
+ setFocusValue: (value: string) => void
+}
+
+function getHandleKeyDownCapture(props: GetHandleKeyDownCaptureArgs) {
+ const { open, value, timeoutIdRef, setOpen, setFocusValue } = props
+
+ return (event: React.KeyboardEvent) => {
+ if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && !open) {
+ setOpen(true)
+ }
+
+ const canNextItemBeSet = !open && value
+
+ if (!canNextItemBeSet) return
+
+ timeoutIdRef.current = setTimeout(() => {
+ if (!(event.key === 'ArrowDown' || event.key === 'ArrowUp')) return
+
+ const activeItem = document.querySelector(ACTIVE_ITEM_CLASSNAME)
+
+ if (!activeItem) return
+
+ const direction = event.key === 'ArrowDown' ? 'next' : 'prev'
+ const newItem = getNextOrPreviousItem(activeItem, direction)
+
+ if (newItem && newItem instanceof HTMLElement) {
+ const newItemValue = newItem.dataset.value
+
+ if (!newItemValue) return
+
+ setFocusValue(newItemValue)
+ }
+ }, ZERO)
+ }
+}
+
+export { getHandleKeyDownCapture }
diff --git a/src/components/combobox/utils/get-next-or-previous-item.ts b/src/components/combobox/utils/get-next-or-previous-item.ts
new file mode 100644
index 0000000..e0339b3
--- /dev/null
+++ b/src/components/combobox/utils/get-next-or-previous-item.ts
@@ -0,0 +1,21 @@
+import { ONE, ZERO } from '@renderui/constants'
+
+import { ITEM_CLASSNAME } from '@/components/combobox/constants/constants'
+
+function getNextOrPreviousItem(activeItem: Element, direction: 'next' | 'prev') {
+ const items = Array.from(document.querySelectorAll(ITEM_CLASSNAME))
+ const activeIndex = items.indexOf(activeItem)
+ const lastIndex = items.length - ONE
+
+ if (direction === 'next') {
+ const nextIndex = activeIndex >= lastIndex ? ZERO : activeIndex + ONE
+
+ return items.at(nextIndex)
+ }
+
+ const previousIndex = activeIndex <= ZERO ? lastIndex : activeIndex - ONE
+
+ return items.at(previousIndex)
+}
+
+export { getNextOrPreviousItem }
diff --git a/src/components/command/components/command-dialog.tsx b/src/components/command/components/command-dialog.tsx
new file mode 100644
index 0000000..61a02e4
--- /dev/null
+++ b/src/components/command/components/command-dialog.tsx
@@ -0,0 +1,24 @@
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Command } from '@/components/command/components/command'
+import {
+ COMMAND_DIALOG_COMMAND_CLASSNAME,
+ COMMAND_DIALOG_CONTENT_CLASSNAME,
+} from '@/components/command/constants/constants'
+import { CommandDialogProps } from '@/components/command/types/command-dialog'
+import { Dialog, DialogContent } from '@/components/dialog'
+
+const CommandDialog = (props: CommandDialogProps) => {
+ const { children, ...restProps } = props
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export { CommandDialog }
diff --git a/src/components/command/components/command-empty.tsx b/src/components/command/components/command-empty.tsx
new file mode 100644
index 0000000..5606ccc
--- /dev/null
+++ b/src/components/command/components/command-empty.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@renderui/utils'
+import { CommandEmpty as CommandEmptyPrimitive } from 'cmdk'
+import React from 'react'
+
+import { DEFAULT_COMMAND_EMPTY_CLASSNAME } from '@/components/command/constants/constants'
+import { CommandEmptyProps, CommandEmptyRef } from '@/components/command/types/command-empty'
+
+const CommandEmpty = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+CommandEmpty.displayName = 'CommandEmpty'
+
+export { CommandEmpty }
diff --git a/src/components/command/components/command-group.tsx b/src/components/command/components/command-group.tsx
new file mode 100644
index 0000000..5e7235b
--- /dev/null
+++ b/src/components/command/components/command-group.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@renderui/utils'
+import { CommandGroup as CommandGroupPrimitive } from 'cmdk'
+import React from 'react'
+
+import { DEFAULT_COMMAND_GROUP_CLASSNAME } from '@/components/command/constants/constants'
+import { CommandGroupProps, CommandGroupRef } from '@/components/command/types/command-group'
+
+const CommandGroup = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+CommandGroup.displayName = 'CommandGroup'
+
+export { CommandGroup }
diff --git a/src/components/command/components/command-input.tsx b/src/components/command/components/command-input.tsx
new file mode 100644
index 0000000..ec8de60
--- /dev/null
+++ b/src/components/command/components/command-input.tsx
@@ -0,0 +1,84 @@
+'use client'
+
+import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
+import { useControllableState } from '@renderui/hooks'
+import { cn, getOptionalObject, polymorphic } from '@renderui/utils'
+import { CommandInput as CommandInputPrimitive } from 'cmdk'
+import React from 'react'
+
+import {
+ COMMAND_INPUT_CLASSNAME,
+ COMMAND_INPUT_CONTAINER_CLASSNAME,
+ COMMAND_INPUT_ICON_CLASSNAME,
+} from '@/components/command/constants/constants'
+import { useSearch } from '@/components/command/hooks/use-search'
+import { CommandInputProps, CommandInputRef } from '@/components/command/types/command-input'
+
+const CommandInput = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ className,
+ containerProps,
+ iconProps,
+ onValueChange,
+ value: valueProp,
+ defaultValue = '',
+ ...restProps
+ } = props
+
+ const {
+ asChild: inputContainerAsChild,
+ className: inputContainerClassName,
+ ...restInputContainerProps
+ } = getOptionalObject(containerProps)
+
+ const { className: iconClassName, ...restIconProps } = getOptionalObject(iconProps)
+
+ const [value, setValue] = useControllableState({
+ prop: valueProp,
+ defaultProp: defaultValue as string,
+ onChange: onValueChange,
+ })
+
+ const { type, handleValueChangeWithSearch } = useSearch(value, setValue)
+
+ const InputContainerComponent = polymorphic(inputContainerAsChild, 'div')
+
+ const InputComponent = polymorphic(asChild, 'input')
+
+ return (
+
+
+ {type === 'select' ? (
+ handleValueChangeWithSearch(event.target.value)}
+ className={cn(COMMAND_INPUT_CLASSNAME, className)}
+ {...restProps}
+ />
+ ) : (
+
+ )}
+
+ )
+})
+
+CommandInput.displayName = 'CommandInput'
+
+export { CommandInput }
diff --git a/src/components/command/components/command-item.tsx b/src/components/command/components/command-item.tsx
new file mode 100644
index 0000000..ab43c8f
--- /dev/null
+++ b/src/components/command/components/command-item.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@renderui/utils'
+import { CommandItem as CommandItemPrimitive } from 'cmdk'
+import React from 'react'
+
+import { DEFAULT_COMMAND_ITEM_CLASSNAME } from '@/components/command/constants/constants'
+import { CommandItemProps, CommandItemRef } from '@/components/command/types/command-item'
+
+const CommandItem = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+CommandItem.displayName = 'CommandItem'
+
+export { CommandItem }
diff --git a/src/components/command/components/command-list.tsx b/src/components/command/components/command-list.tsx
new file mode 100644
index 0000000..1437656
--- /dev/null
+++ b/src/components/command/components/command-list.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@renderui/utils'
+import { CommandList as CommandListPrimitive } from 'cmdk'
+import React from 'react'
+
+import { DEFAULT_COMMAND_LIST_CLASSNAME } from '@/components/command/constants/constants'
+import { CommandListProps, CommandListRef } from '@/components/command/types/command-list'
+
+const CommandList = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+CommandList.displayName = 'CommandList'
+
+export { CommandList }
diff --git a/src/components/command/components/command.tsx b/src/components/command/components/command.tsx
new file mode 100644
index 0000000..ae0ed0a
--- /dev/null
+++ b/src/components/command/components/command.tsx
@@ -0,0 +1,52 @@
+'use client'
+
+import { useControllableState } from '@renderui/hooks'
+import { cn } from '@renderui/utils'
+import { Command as CommandPrimitive } from 'cmdk'
+import React from 'react'
+
+import { DEFAULT_COMMAND_CLASSNAME } from '@/components/command/constants/constants'
+import { CommandProvider } from '@/components/command/contexts/command-context'
+import { CommandProps, CommandRef } from '@/components/command/types/command'
+import { defaultFilter } from '@/components/command/utils/default-filter'
+
+const Command = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ value: valueProp,
+ defaultValue,
+ onValueChange,
+ filter,
+ loop = true,
+ type = 'combobox',
+ ...restProps
+ } = props
+
+ const [value, setValue] = useControllableState({
+ prop: valueProp,
+ defaultProp: defaultValue,
+ onChange: onValueChange,
+ })
+
+ const memoizedProviderValue = React.useMemo(() => ({ type, setValue }), [type, setValue])
+
+ return (
+
+ {children}
+
+ )
+})
+
+Command.displayName = 'Command'
+
+export { Command }
diff --git a/src/components/command/constants/constants.ts b/src/components/command/constants/constants.ts
new file mode 100644
index 0000000..9c50a78
--- /dev/null
+++ b/src/components/command/constants/constants.ts
@@ -0,0 +1,51 @@
+const DEFAULT_COMMAND_CLASSNAME =
+ 'render-ui-command flex h-full w-full flex-col overflow-hidden rounded-md bg-background text-foreground'
+
+const COMMAND_DIALOG_CONTENT_CLASSNAME = 'overflow-hidden p-0'
+
+const COMMAND_DIALOG_COMMAND_CLASSNAME =
+ 'render-ui-command-dialog-command [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'
+
+const COMMAND_INPUT_CONTAINER_CLASSNAME =
+ 'render-ui-command-input-container flex justify-start items-center border-b border-mode-accent px-3'
+
+const COMMAND_INPUT_CLASSNAME =
+ 'render-ui-command-input flex h-10 rounded-md bg-transparent min-w-[0px] shrink py-3 text-sm w-full outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50'
+
+const COMMAND_INPUT_ICON_CLASSNAME = 'render-ui-command-input-icon mr-2 h-4 w-4 shrink-0 opacity-50'
+
+const DEFAULT_COMMAND_LIST_CLASSNAME =
+ 'render-ui-command-list max-h-[300px] overflow-y-auto overflow-x-hidden'
+
+const DEFAULT_COMMAND_EMPTY_CLASSNAME = 'render-ui-command-dialog-empty py-6 text-center text-sm'
+
+const DEFAULT_COMMAND_GROUP_CLASSNAME =
+ 'render-ui-command-dialog-group overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground'
+
+const DEFAULT_COMMAND_ITEM_CLASSNAME =
+ 'render-ui-command-item relative rounded flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none aria-selected:bg-primary aria-selected:text-white data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
+
+const RADIX_FOCUS_GUARD_ATTRUBUTE = '[data-radix-focus-guard]'
+
+const COMMAND_ATTRIBUTE = '[data-command-popover-root]'
+
+const COMMAND_ITEM_CLASSNAME_SELECTOR = '.render-ui-command-item'
+
+const SEARCH_PAUSE_DURATION = 750
+
+export {
+ COMMAND_ATTRIBUTE,
+ COMMAND_DIALOG_COMMAND_CLASSNAME,
+ COMMAND_DIALOG_CONTENT_CLASSNAME,
+ COMMAND_INPUT_CLASSNAME,
+ COMMAND_INPUT_CONTAINER_CLASSNAME,
+ COMMAND_INPUT_ICON_CLASSNAME,
+ COMMAND_ITEM_CLASSNAME_SELECTOR,
+ DEFAULT_COMMAND_CLASSNAME,
+ DEFAULT_COMMAND_EMPTY_CLASSNAME,
+ DEFAULT_COMMAND_GROUP_CLASSNAME,
+ DEFAULT_COMMAND_ITEM_CLASSNAME,
+ DEFAULT_COMMAND_LIST_CLASSNAME,
+ RADIX_FOCUS_GUARD_ATTRUBUTE,
+ SEARCH_PAUSE_DURATION,
+}
diff --git a/src/components/command/contexts/command-context.ts b/src/components/command/contexts/command-context.ts
new file mode 100644
index 0000000..c58317b
--- /dev/null
+++ b/src/components/command/contexts/command-context.ts
@@ -0,0 +1,12 @@
+import { initializeContext } from '@renderui/utils'
+
+import { CommandContext } from '@/components/command/types/command-context'
+
+const [CommandProvider, useCommandContext] = initializeContext({
+ errorMessage: 'Components using command context must be wrapped in a .',
+ providerName: 'CommandProvider',
+ hookName: 'useCommandContext',
+ name: 'CommandContext',
+})
+
+export { CommandProvider, useCommandContext }
diff --git a/src/components/command/hooks/use-command.ts b/src/components/command/hooks/use-command.ts
new file mode 100644
index 0000000..7a53ecd
--- /dev/null
+++ b/src/components/command/hooks/use-command.ts
@@ -0,0 +1,20 @@
+import React from 'react'
+
+import { DEFAULT_COMMAND_CLASSNAME } from '@/packages/components/command/constants/constants'
+import { CommandProps, CommandRef } from '@/packages/components/command/types/command'
+import { defaultFilter } from '@/packages/components/command/utils/default-filter'
+import { cn } from '@/packages/utils/cn'
+
+function useCommand(props: CommandProps, ref: React.ForwardedRef) {
+ const { className, onValueChange, filter, ...restProps } = props
+
+ return {
+ ref,
+ className: cn(DEFAULT_COMMAND_CLASSNAME, className),
+ onValueChange,
+ filter: filter ?? defaultFilter,
+ ...restProps,
+ }
+}
+
+export { useCommand }
diff --git a/src/components/command/hooks/use-search.ts b/src/components/command/hooks/use-search.ts
new file mode 100644
index 0000000..0ef7fba
--- /dev/null
+++ b/src/components/command/hooks/use-search.ts
@@ -0,0 +1,125 @@
+import { ZERO } from '@renderui/constants'
+import { useMutationObserver } from '@renderui/hooks'
+import React from 'react'
+
+import {
+ COMMAND_ATTRIBUTE,
+ COMMAND_ITEM_CLASSNAME_SELECTOR,
+ RADIX_FOCUS_GUARD_ATTRUBUTE,
+ SEARCH_PAUSE_DURATION,
+} from '@/components/command/constants/constants'
+import { useCommandContext } from '@/components/command/contexts/command-context'
+import { lowercaseBinarySearch } from '@/components/command/utils/lowercase-binary-search'
+
+function useSearch(
+ value: string,
+ setValue: React.Dispatch>,
+) {
+ const dataValueMapRef = React.useRef>(new Map())
+ const dataValueArrayRef = React.useRef([])
+ const timeoutIdRef = React.useRef(null)
+ const previousSuccesfulSearchRef = React.useRef('')
+ const shouldStartClearOnNextInputRef = React.useRef(false)
+
+ const { type, setValue: setRootValue } = useCommandContext()
+
+ const storeCommandItems = React.useCallback(() => {
+ /* check if the focus guard exists and the command root exists */
+ const focusGuardExists = document.querySelector(RADIX_FOCUS_GUARD_ATTRUBUTE) !== null
+ const cmdkRoot = document.querySelector(COMMAND_ATTRIBUTE)
+
+ if (!focusGuardExists || !cmdkRoot) return
+
+ /* find all command popover items in the DOM */
+ const commandItems = cmdkRoot.querySelectorAll(COMMAND_ITEM_CLASSNAME_SELECTOR)
+ const newDataValueMap = new Map()
+ const newDataValueArray: string[] = []
+
+ commandItems.forEach((item) => {
+ if (!(item instanceof HTMLElement)) return
+
+ const dataValue = item.dataset.label || ''
+
+ newDataValueMap.set(dataValue, dataValue)
+ newDataValueArray.push(dataValue)
+ })
+
+ /* set map of found items */
+ dataValueMapRef.current = newDataValueMap
+
+ /* set sorted array of found items, will be binary searched */
+ dataValueArrayRef.current = newDataValueArray.sort((a: string, b: string) =>
+ a.toLowerCase().localeCompare(b.toLowerCase()),
+ )
+ }, [])
+
+ useMutationObserver(document.documentElement, storeCommandItems, {
+ childList: true,
+ subtree: true,
+ attributeOldValue: false,
+ characterData: false,
+ attributes: false,
+ characterDataOldValue: false,
+ })
+
+ React.useEffect(() => {
+ setTimeout(storeCommandItems, ZERO)
+ }, [storeCommandItems])
+
+ const handleValueChangeWithSearch = (searchValue: string) => {
+ /* initial check */
+ if (!setValue) return
+
+ console.log(searchValue)
+
+ let currentSearchValue = searchValue
+
+ setValue(currentSearchValue)
+
+ /* clear previous search reset timeout */
+ if (timeoutIdRef.current) {
+ clearTimeout(timeoutIdRef.current)
+ }
+
+ /* add new search reset timeout */
+ timeoutIdRef.current = setTimeout(() => {
+ shouldStartClearOnNextInputRef.current = true
+ }, SEARCH_PAUSE_DURATION)
+
+ /* if input was truthy but cleared, do not change root value, stay on current item */
+ if (currentSearchValue?.trim() === '') return
+
+ /* if enough time passed, shave the previous string from the current string */
+ if (shouldStartClearOnNextInputRef.current && value) {
+ currentSearchValue = searchValue.startsWith(value)
+ ? searchValue.slice(value.length)
+ : searchValue
+ shouldStartClearOnNextInputRef.current = false
+ }
+
+ /* set new input value, after input manipulation */
+ setValue(currentSearchValue)
+
+ /* check for exact match first with map */
+ if (dataValueMapRef.current.has(currentSearchValue)) {
+ setRootValue(currentSearchValue)
+
+ return
+ }
+
+ /* check for label that starts with input using lowercase binary search */
+ const startingLabelElement = lowercaseBinarySearch(
+ dataValueArrayRef.current,
+ currentSearchValue,
+ )
+
+ if (startingLabelElement) {
+ setRootValue(startingLabelElement)
+ previousSuccesfulSearchRef.current = startingLabelElement
+ }
+ }
+
+ return { type, handleValueChangeWithSearch }
+}
+
+export { useSearch }
diff --git a/src/components/command/index.ts b/src/components/command/index.ts
new file mode 100644
index 0000000..9aa8b96
--- /dev/null
+++ b/src/components/command/index.ts
@@ -0,0 +1,14 @@
+export * from '@/components/command/components/command'
+export * from '@/components/command/components/command-dialog'
+export * from '@/components/command/components/command-empty'
+export * from '@/components/command/components/command-group'
+export * from '@/components/command/components/command-input'
+export * from '@/components/command/components/command-item'
+export * from '@/components/command/components/command-list'
+export * from '@/components/command/types/command'
+export * from '@/components/command/types/command-dialog'
+export * from '@/components/command/types/command-empty'
+export * from '@/components/command/types/command-group'
+export * from '@/components/command/types/command-input'
+export * from '@/components/command/types/command-item'
+export * from '@/components/command/types/command-list'
diff --git a/src/components/command/types/command-context.ts b/src/components/command/types/command-context.ts
new file mode 100644
index 0000000..401d2b0
--- /dev/null
+++ b/src/components/command/types/command-context.ts
@@ -0,0 +1,8 @@
+import { CommandProps } from '@/components/command/types/command'
+
+type CommandContext = {
+ type: CommandProps['type']
+ setValue: React.Dispatch>
+}
+
+export type { CommandContext }
diff --git a/src/components/command/types/command-dialog.ts b/src/components/command/types/command-dialog.ts
new file mode 100644
index 0000000..5687c67
--- /dev/null
+++ b/src/components/command/types/command-dialog.ts
@@ -0,0 +1,3 @@
+type CommandDialogProps = any
+
+export type { CommandDialogProps }
diff --git a/src/components/command/types/command-empty.ts b/src/components/command/types/command-empty.ts
new file mode 100644
index 0000000..294067a
--- /dev/null
+++ b/src/components/command/types/command-empty.ts
@@ -0,0 +1,9 @@
+import { CommandEmpty as CommandEmptyPrimitive } from 'cmdk'
+
+type CommandEmptyPrimitiveType = typeof CommandEmptyPrimitive
+
+type CommandEmptyRef = React.ElementRef
+
+type CommandEmptyProps = React.ComponentPropsWithoutRef
+
+export type { CommandEmptyProps, CommandEmptyRef }
diff --git a/src/components/command/types/command-group.ts b/src/components/command/types/command-group.ts
new file mode 100644
index 0000000..9adb6e7
--- /dev/null
+++ b/src/components/command/types/command-group.ts
@@ -0,0 +1,9 @@
+import { CommandGroup as CommandGroupPrimitive } from 'cmdk'
+
+type CommandGroupPrimitiveType = typeof CommandGroupPrimitive
+
+type CommandGroupRef = React.ElementRef
+
+type CommandGroupProps = React.ComponentPropsWithoutRef
+
+export type { CommandGroupProps, CommandGroupRef }
diff --git a/src/components/command/types/command-input.ts b/src/components/command/types/command-input.ts
new file mode 100644
index 0000000..338c243
--- /dev/null
+++ b/src/components/command/types/command-input.ts
@@ -0,0 +1,22 @@
+import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
+import { Simplify } from '@renderui/types'
+import { CommandInput as CommandInputPrimitive } from 'cmdk'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type CommandInputPrimitiveType = typeof CommandInputPrimitive
+
+type CommandInputRef = React.ElementRef
+
+type CommandInputPrimitiveProps = React.ComponentPropsWithoutRef
+
+type ComboboxInputCustomProps = {
+ containerProps?: Simplify & AsChildProp>
+ iconProps?: React.ComponentPropsWithoutRef
+}
+
+type CommandInputProps = Simplify<
+ CommandInputPrimitiveProps & ComboboxInputCustomProps & AsChildProp
+>
+
+export type { CommandInputProps, CommandInputRef }
diff --git a/src/components/command/types/command-item.ts b/src/components/command/types/command-item.ts
new file mode 100644
index 0000000..7e84187
--- /dev/null
+++ b/src/components/command/types/command-item.ts
@@ -0,0 +1,9 @@
+import { CommandItem as CommandItemPrimitive } from 'cmdk'
+
+type CommandItemPrimitiveType = typeof CommandItemPrimitive
+
+type CommandItemRef = React.ElementRef
+
+type CommandItemProps = React.ComponentPropsWithoutRef
+
+export type { CommandItemProps, CommandItemRef }
diff --git a/src/components/command/types/command-list.ts b/src/components/command/types/command-list.ts
new file mode 100644
index 0000000..d8df7d3
--- /dev/null
+++ b/src/components/command/types/command-list.ts
@@ -0,0 +1,9 @@
+import { CommandList as CommandListPrimitive } from 'cmdk'
+
+type CommandListPrimitiveType = typeof CommandListPrimitive
+
+type CommandListRef = React.ElementRef
+
+type CommandListProps = React.ComponentPropsWithoutRef
+
+export type { CommandListProps, CommandListRef }
diff --git a/src/components/command/types/command.ts b/src/components/command/types/command.ts
new file mode 100644
index 0000000..559e6e9
--- /dev/null
+++ b/src/components/command/types/command.ts
@@ -0,0 +1,18 @@
+import { Simplify } from '@renderui/types'
+import { Command as CommandPrimitive } from 'cmdk'
+import React from 'react'
+
+
+type CommandPrimitiveType = typeof CommandPrimitive
+
+type CommandRef = React.ElementRef
+
+type CommandPrimitiveProps = React.ComponentPropsWithoutRef
+
+type CommandCustomProps = {
+ type?: 'select' | 'combobox'
+}
+
+type CommandProps = Simplify
+
+export type { CommandProps, CommandRef }
diff --git a/src/components/command/utils/default-filter.ts b/src/components/command/utils/default-filter.ts
new file mode 100644
index 0000000..886ed68
--- /dev/null
+++ b/src/components/command/utils/default-filter.ts
@@ -0,0 +1,9 @@
+import { ONE, ZERO } from '@renderui/constants'
+
+function defaultFilter(value: string, search: string) {
+ if (value.toLowerCase().startsWith(search.toLowerCase())) return ONE
+
+ return ZERO
+}
+
+export { defaultFilter }
diff --git a/src/components/command/utils/filter-with-focus-on-exact-match.ts b/src/components/command/utils/filter-with-focus-on-exact-match.ts
new file mode 100644
index 0000000..05bb1c7
--- /dev/null
+++ b/src/components/command/utils/filter-with-focus-on-exact-match.ts
@@ -0,0 +1,41 @@
+import { ONE, ZERO } from '@renderui/constants'
+import React from 'react'
+
+import { defaultFilter } from '@/components/command/utils/default-filter'
+
+type GetFilterWithFocusOnExactMatchArgs = {
+ timeoutIdRef: React.MutableRefObject
+ dataValueMapRef: React.MutableRefObject>
+ dataValueArrayRef: React.MutableRefObject
+ onValueChange: ((value: string) => void) | undefined
+ filter: ((value: string, search: string) => number) | undefined
+}
+
+function getFilterWithFocusOnExactMatch(args: GetFilterWithFocusOnExactMatchArgs) {
+ const { timeoutIdRef, dataValueMapRef, dataValueArrayRef, onValueChange, filter } = args
+
+ return (filterValue: string, filterSearch: string) => {
+ timeoutIdRef.current = setTimeout(() => {
+ if (dataValueMapRef.current.has(filterSearch)) {
+ onValueChange?.(filterSearch)
+ }
+ }, ZERO)
+
+ if (filterSearch !== '') {
+ const startingLabelElement = dataValueArrayRef.current.find((value) =>
+ value.toLowerCase().startsWith(filterSearch.toLowerCase()),
+ )
+
+ if (startingLabelElement) onValueChange?.(startingLabelElement)
+ }
+
+ const isMatch =
+ filter?.(filterValue, filterSearch) || defaultFilter?.(filterValue, filterSearch)
+
+ if (isMatch) return ONE
+
+ return ZERO
+ }
+}
+
+export { getFilterWithFocusOnExactMatch }
diff --git a/src/components/command/utils/lowercase-binary-search.ts b/src/components/command/utils/lowercase-binary-search.ts
new file mode 100644
index 0000000..2acef45
--- /dev/null
+++ b/src/components/command/utils/lowercase-binary-search.ts
@@ -0,0 +1,30 @@
+import { ONE, TWO, ZERO } from '@renderui/constants'
+
+function lowercaseBinarySearch(array: string[], target: string): string | undefined {
+ let left = ZERO
+ let right = array.length - ONE
+
+ const lowerCaseTarget = target.toLowerCase()
+
+ while (left <= right) {
+ const mid = Math.floor((left + right) / TWO)
+ const midValue = array[mid].toLowerCase()
+
+ if (midValue.startsWith(lowerCaseTarget)) {
+ // If the previous element also starts with the target, continue the search
+ if (mid > ZERO && array[mid - ONE].toLowerCase().startsWith(lowerCaseTarget)) {
+ right = mid - ONE
+ } else {
+ return array[mid] // return original value, not lowercased
+ }
+ } else if (midValue < lowerCaseTarget) {
+ left = mid + ONE
+ } else {
+ right = mid - ONE
+ }
+ }
+
+ return undefined
+}
+
+export { lowercaseBinarySearch }
diff --git a/src/components/container/components/container.tsx b/src/components/container/components/container.tsx
new file mode 100644
index 0000000..2240dde
--- /dev/null
+++ b/src/components/container/components/container.tsx
@@ -0,0 +1,19 @@
+import { polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { ContainerProps, ContainerRef } from '@/components/container/types/container'
+import { getMergedClassName } from '@/components/container/utils/get-merged-class-name'
+
+const Container = React.forwardRef((props, ref) => {
+ const { asChild, isFullHeight, className, ...restProps } = props
+
+ const Component = polymorphic(asChild, 'div')
+
+ return (
+
+ )
+})
+
+Container.displayName = 'Container'
+
+export { Container }
diff --git a/src/components/container/index.ts b/src/components/container/index.ts
new file mode 100644
index 0000000..6edf074
--- /dev/null
+++ b/src/components/container/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/container/components/container'
+export * from '@/components/container/types/container'
diff --git a/src/components/container/types/container.ts b/src/components/container/types/container.ts
new file mode 100644
index 0000000..0933702
--- /dev/null
+++ b/src/components/container/types/container.ts
@@ -0,0 +1,16 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '../../_shared/types/as-child'
+
+type ContainerRef = React.ElementRef<'div'>
+
+type ContainerPrimitiveProps = React.ComponentPropsWithoutRef<'div'>
+
+type ContainerRenderUIProps = {
+ isFullHeight?: boolean
+}
+
+type ContainerProps = Simplify
+
+export type { ContainerProps, ContainerRef }
diff --git a/src/components/container/utils/get-merged-class-name.ts b/src/components/container/utils/get-merged-class-name.ts
new file mode 100644
index 0000000..e0b4d47
--- /dev/null
+++ b/src/components/container/utils/get-merged-class-name.ts
@@ -0,0 +1,16 @@
+import { cn } from '@renderui/utils'
+
+import { ContainerProps } from '@/components/container/types/container'
+
+function getMergedClassName(
+ isFullHeight: ContainerProps['isFullHeight'],
+ className: ContainerProps['className'],
+) {
+ return cn(
+ 'render-ui-container 2xl:max-w-screen-2xl px-4 md:px-6 lg:px-8',
+ isFullHeight ? 'min-h-screen' : '',
+ className,
+ )
+}
+
+export { getMergedClassName }
diff --git a/src/components/dialog/components/dialog-close.tsx b/src/components/dialog/components/dialog-close.tsx
new file mode 100644
index 0000000..6d7ddc5
--- /dev/null
+++ b/src/components/dialog/components/dialog-close.tsx
@@ -0,0 +1,7 @@
+import { ModalClose } from '@/components/_shared/components/modal-close/modal-close'
+
+const DialogClose = ModalClose
+
+DialogClose.displayName = 'DialogClose'
+
+export { DialogClose }
diff --git a/src/components/dialog/components/dialog-content.tsx b/src/components/dialog/components/dialog-content.tsx
new file mode 100644
index 0000000..6578289
--- /dev/null
+++ b/src/components/dialog/components/dialog-content.tsx
@@ -0,0 +1,77 @@
+import {
+ DialogContent as DialogContentPrimitive,
+ DialogPortal as DialogPortalPrimitive,
+} from '@radix-ui/react-dialog'
+import { Cross2Icon } from '@radix-ui/react-icons'
+import { cn, cx, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button/components/button'
+import { DialogClose } from '@/components/dialog/components/dialog-close'
+import {
+ DEFAULT_DIALOG_CLOSE_BUTTON_CLASSNAME,
+ DEFAULT_DIALOG_CLOSE_BUTTON_ICON_CLASSNAME,
+ DFEAULT_DIALOG_CONTENT_CLASSNAME,
+} from '@/components/dialog/constants/constants'
+import { DialogContentProps, DialogContentRef } from '@/components/dialog/types/dialog-content'
+import { Overlay } from '@/components/overlay/components/overlay'
+import { VisuallyHidden } from '@/components/visually-hidden/components/visually-hidden'
+
+const DialogContent = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ closeButtonProps,
+ closeButtonIconProps,
+ hasCloseButton = true,
+ ...restProps
+ } = props
+
+ const {
+ 'className': closeButtonClassName,
+ 'aria-label': closeButtonAriaLabel,
+ variant = 'ghost',
+ color = 'mode-contrast',
+ ...restCloseButtonProps
+ } = getOptionalObject(closeButtonProps)
+
+ const { className: closeButtonIconClassName, ...restCloseButtonIconProps } =
+ getOptionalObject(closeButtonIconProps)
+
+ return (
+
+
+
+ {hasCloseButton ? (
+
+
+
+ {closeButtonAriaLabel ? null : Close }
+
+
+ ) : null}
+ {children}
+
+
+ )
+})
+
+DialogContent.displayName = 'DialogContent'
+
+export { DialogContent }
diff --git a/src/components/dialog/components/dialog-description.tsx b/src/components/dialog/components/dialog-description.tsx
new file mode 100644
index 0000000..872bccc
--- /dev/null
+++ b/src/components/dialog/components/dialog-description.tsx
@@ -0,0 +1,7 @@
+import { ModalDescription } from '@/components/_shared/components/modal-description/modal-description'
+
+const DialogDescription = ModalDescription
+
+DialogDescription.displayName = 'DialogDescription'
+
+export { DialogDescription }
diff --git a/src/components/dialog/components/dialog-footer.tsx b/src/components/dialog/components/dialog-footer.tsx
new file mode 100644
index 0000000..aa90378
--- /dev/null
+++ b/src/components/dialog/components/dialog-footer.tsx
@@ -0,0 +1,7 @@
+import { ModalFooter } from '@/components/_shared/components/modal-footer/modal-footer'
+
+const DialogFooter = ModalFooter
+
+DialogFooter.displayName = 'DialogFooter'
+
+export { DialogFooter }
diff --git a/src/components/dialog/components/dialog-header.tsx b/src/components/dialog/components/dialog-header.tsx
new file mode 100644
index 0000000..32ed41e
--- /dev/null
+++ b/src/components/dialog/components/dialog-header.tsx
@@ -0,0 +1,7 @@
+import { ModalHeader } from '@/components/_shared/components/modal-header/modal-header'
+
+const DialogHeader = ModalHeader
+
+DialogHeader.displayName = 'DialogHeader'
+
+export { DialogHeader }
diff --git a/src/components/dialog/components/dialog-title.tsx b/src/components/dialog/components/dialog-title.tsx
new file mode 100644
index 0000000..2c711e3
--- /dev/null
+++ b/src/components/dialog/components/dialog-title.tsx
@@ -0,0 +1,7 @@
+import { ModalTitle } from '@/components/_shared/components/modal-title/modal-title'
+
+const DialogTitle = ModalTitle
+
+DialogTitle.displayName = 'DialogTitle'
+
+export { DialogTitle }
diff --git a/src/components/dialog/components/dialog-trigger.tsx b/src/components/dialog/components/dialog-trigger.tsx
new file mode 100644
index 0000000..a98ae36
--- /dev/null
+++ b/src/components/dialog/components/dialog-trigger.tsx
@@ -0,0 +1,28 @@
+import { DialogTrigger as DialogTriggerPrimitive } from '@radix-ui/react-dialog'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button/components/button'
+import { DEFAULT_DIALOG_TRIGGER_CLASSNAME } from '@/components/dialog/constants/constants'
+import { DialogTriggerProps, DialogTriggerRef } from '@/components/dialog/types/dialog-trigger'
+
+const DialogTrigger = React.forwardRef((props, ref) => {
+ const { className, variant = 'solid', color = 'mode-accent', ...restProps } = props
+
+ return (
+
+
+
+ )
+})
+
+DialogTrigger.displayName = 'DialogTrigger'
+
+export { DialogTrigger }
diff --git a/src/components/dialog/components/dialog.tsx b/src/components/dialog/components/dialog.tsx
new file mode 100644
index 0000000..a9d835d
--- /dev/null
+++ b/src/components/dialog/components/dialog.tsx
@@ -0,0 +1,14 @@
+import { Dialog as DialogPrimitive } from '@radix-ui/react-dialog'
+import React from 'react'
+
+import { DialogProps } from '@/components/dialog/types/dialog'
+
+const Dialog = (props: DialogProps) => {
+ const { isModal, ...restProps } = props
+
+ return
+}
+
+Dialog.displayName = 'Dialog'
+
+export { Dialog }
diff --git a/src/components/dialog/constants/constants.ts b/src/components/dialog/constants/constants.ts
new file mode 100644
index 0000000..5b0af04
--- /dev/null
+++ b/src/components/dialog/constants/constants.ts
@@ -0,0 +1,17 @@
+const DEFAULT_DIALOG_TRIGGER_CLASSNAME = 'render-ui-dialog-trigger rounded'
+
+const DFEAULT_DIALOG_CONTENT_CLASSNAME =
+ 'render-ui-dialog-content fixed left-[50%] top-[50%] z-50 grid w-[calc(100%_-_1rem)] sm:w-full sm:max-w-lg -translate-x-1/2 -translate-y-1/2 text-mode-contrast border border-mode-accent gap-4 bg-background p-6 shadow-lg duration-medium data-[state=open]:animate-dialog-enter data-[state=closed]:animate-dialog-exit data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg outline-none ring-ring-color ring-offset-background focus-visible:ring-ring-width focus-visible:ring-offset-offset'
+
+const DEFAULT_DIALOG_CLOSE_BUTTON_CLASSNAME =
+ 'redner-ui-dialog-close absolute z-[1] right-4 top-4 p-2'
+
+const DEFAULT_DIALOG_CLOSE_BUTTON_ICON_CLASSNAME =
+ 'render-ui-sheet-close-button-icon h-4 w-4 opacity-70'
+
+export {
+ DEFAULT_DIALOG_CLOSE_BUTTON_CLASSNAME,
+ DEFAULT_DIALOG_CLOSE_BUTTON_ICON_CLASSNAME,
+ DEFAULT_DIALOG_TRIGGER_CLASSNAME,
+ DFEAULT_DIALOG_CONTENT_CLASSNAME,
+}
diff --git a/src/components/dialog/index.ts b/src/components/dialog/index.ts
new file mode 100644
index 0000000..85f048c
--- /dev/null
+++ b/src/components/dialog/index.ts
@@ -0,0 +1,11 @@
+export * from '@/components/dialog/components/dialog'
+export * from '@/components/dialog/components/dialog-close'
+export * from '@/components/dialog/components/dialog-content'
+export * from '@/components/dialog/components/dialog-description'
+export * from '@/components/dialog/components/dialog-footer'
+export * from '@/components/dialog/components/dialog-header'
+export * from '@/components/dialog/components/dialog-title'
+export * from '@/components/dialog/components/dialog-trigger'
+export * from '@/components/dialog/types/dialog'
+export * from '@/components/dialog/types/dialog-content'
+export * from '@/components/dialog/types/dialog-trigger'
\ No newline at end of file
diff --git a/src/components/dialog/types/dialog-content.ts b/src/components/dialog/types/dialog-content.ts
new file mode 100644
index 0000000..da06e8c
--- /dev/null
+++ b/src/components/dialog/types/dialog-content.ts
@@ -0,0 +1,27 @@
+import {
+ DialogContent as DialogContentPrimitive,
+ DialogPortal as DialogPortalPrimitive,
+} from '@radix-ui/react-dialog'
+import { Cross2Icon } from '@radix-ui/react-icons'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { ButtonProps } from '@/components/button'
+
+type DialogContentPrimitiveType = typeof DialogContentPrimitive
+
+type DialogContentRef = React.ElementRef
+
+type DialogContentPrimitiveProps = React.ComponentPropsWithRef
+
+type DialogContentRenderUIProps = {
+ portalProps?: React.ComponentPropsWithoutRef
+ overlayProps?: React.ComponentPropsWithoutRef
+ closeButtonProps?: ButtonProps
+ closeButtonIconProps?: React.ComponentPropsWithoutRef
+ hasCloseButton?: boolean
+}
+
+type DialogContentProps = Simplify
+
+export type { DialogContentProps, DialogContentRef }
diff --git a/src/components/dialog/types/dialog-trigger.ts b/src/components/dialog/types/dialog-trigger.ts
new file mode 100644
index 0000000..51f1911
--- /dev/null
+++ b/src/components/dialog/types/dialog-trigger.ts
@@ -0,0 +1,7 @@
+import { ButtonProps, ButtonRef } from '@/components/button'
+
+type DialogTriggerRef = ButtonRef
+
+type DialogTriggerProps = ButtonProps
+
+export type { DialogTriggerProps, DialogTriggerRef }
diff --git a/src/components/dialog/types/dialog.ts b/src/components/dialog/types/dialog.ts
new file mode 100644
index 0000000..9421935
--- /dev/null
+++ b/src/components/dialog/types/dialog.ts
@@ -0,0 +1,13 @@
+import { Dialog as DialogPrimitive } from '@radix-ui/react-dialog'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+type DialogPrimitiveProps = Omit, 'modal'>
+
+type DialogCustomProps = {
+ isModal?: boolean
+}
+
+type DialogProps = Simplify
+
+export type { DialogProps }
diff --git a/src/components/flex/components/flex.tsx b/src/components/flex/components/flex.tsx
new file mode 100644
index 0000000..86596d3
--- /dev/null
+++ b/src/components/flex/components/flex.tsx
@@ -0,0 +1,25 @@
+import { polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { FlexProps, FlexRef } from '@/components/flex/types/flex'
+import { getMergedClassName } from '@/components/flex/utils/get-merged-class-name'
+
+const Flex = React.forwardRef((props, ref) => {
+ const { asChild, children, growChildren, center, className, ...restProps } = props
+
+ const Component = polymorphic(asChild, 'div')
+
+ return (
+
+ {children}
+
+ )
+})
+
+Flex.displayName = 'Flex'
+
+export { Flex }
diff --git a/src/components/flex/index.ts b/src/components/flex/index.ts
new file mode 100644
index 0000000..1f98f05
--- /dev/null
+++ b/src/components/flex/index.ts
@@ -0,0 +1,2 @@
+export * from './components/flex'
+export * from './types/flex'
diff --git a/src/components/flex/types/flex.ts b/src/components/flex/types/flex.ts
new file mode 100644
index 0000000..f5d0565
--- /dev/null
+++ b/src/components/flex/types/flex.ts
@@ -0,0 +1,17 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type FlexRef = React.ElementRef<'div'>
+
+type FlexPropsPrimitiveProps = React.ComponentPropsWithoutRef<'div'>
+
+type FlexboxCustomProps = {
+ growChildren?: boolean
+ center?: boolean
+}
+
+type FlexProps = Simplify
+
+export type { FlexProps, FlexRef }
diff --git a/src/components/flex/utils/get-merged-class-name.ts b/src/components/flex/utils/get-merged-class-name.ts
new file mode 100644
index 0000000..2e81c22
--- /dev/null
+++ b/src/components/flex/utils/get-merged-class-name.ts
@@ -0,0 +1,18 @@
+import { cn } from '@renderui/utils'
+
+import { FlexProps } from '@/components/flex/types/flex'
+
+function getMergedClassName(
+ growChildren: FlexProps['growChildren'],
+ center: FlexProps['center'],
+ className: FlexProps['className'],
+) {
+ return cn(
+ 'render-ui-flex flex',
+ growChildren ? 'grow-children' : '',
+ center ? 'center' : '',
+ className,
+ )
+}
+
+export { getMergedClassName }
diff --git a/src/components/form/components/form.tsx b/src/components/form/components/form.tsx
new file mode 100644
index 0000000..df178c5
--- /dev/null
+++ b/src/components/form/components/form.tsx
@@ -0,0 +1,31 @@
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { FormProps, FormRef } from '@/components/form/types/form'
+import { getSubmitProps } from '@/components/form/utils/get-submit-props'
+
+const Form = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ className,
+ onSubmit,
+ onSubmitWithFields,
+ isDefaultPreventedOnSubmit = true,
+ ...restProps
+ } = props
+
+ const Component = polymorphic(asChild, 'form')
+
+ return (
+
+ )
+})
+
+Form.displayName = 'Form'
+
+export { Form }
diff --git a/src/components/form/index.ts b/src/components/form/index.ts
new file mode 100644
index 0000000..d19c2c0
--- /dev/null
+++ b/src/components/form/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/form/components/form'
+export * from '@/components/form/types/form'
diff --git a/src/components/form/types/form.ts b/src/components/form/types/form.ts
new file mode 100644
index 0000000..400f68d
--- /dev/null
+++ b/src/components/form/types/form.ts
@@ -0,0 +1,17 @@
+import { Simplify } from '@renderui/types'
+import { FormEvent } from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type FormRef = React.ElementRef<'form'>
+
+type FormPrimitiveProps = React.ComponentPropsWithoutRef<'form'>
+
+type FormRenderUIProps = {
+ isDefaultPreventedOnSubmit?: boolean
+ onSubmitWithFields?: (formData?: { [k: string]: FormDataEntryValue }, event?: FormEvent) => void
+}
+
+type FormProps = Simplify
+
+export { FormProps, FormRef }
diff --git a/src/components/form/utils/get-form-data.ts b/src/components/form/utils/get-form-data.ts
new file mode 100644
index 0000000..7eda57a
--- /dev/null
+++ b/src/components/form/utils/get-form-data.ts
@@ -0,0 +1,12 @@
+function getFormData(event: React.FormEvent) {
+ const formData = new FormData(event.currentTarget)
+ const result: { [key: string]: FormDataEntryValue } = {}
+
+ formData.forEach((value, key) => {
+ result[key] = value
+ })
+
+ return result
+}
+
+export { getFormData }
diff --git a/src/components/form/utils/get-submit-props.ts b/src/components/form/utils/get-submit-props.ts
new file mode 100644
index 0000000..fddc11c
--- /dev/null
+++ b/src/components/form/utils/get-submit-props.ts
@@ -0,0 +1,23 @@
+import { FormProps } from '@/components/form/types/form'
+import { getFormData } from '@/components/form/utils/get-form-data'
+
+/* we are checking if submit handler is attached, if not return undefined to keep server-component functionality */
+const getSubmitProps = (
+ isDefaultPreventedOnSubmit: FormProps['isDefaultPreventedOnSubmit'],
+ onSubmit: FormProps['onSubmit'],
+ onSubmitWithFields: FormProps['onSubmitWithFields'],
+) => {
+ if (!onSubmit && !onSubmitWithFields) return undefined
+
+ return {
+ onSubmit: (event: React.FormEvent) => {
+ if (isDefaultPreventedOnSubmit) event.preventDefault()
+
+ if (onSubmit) onSubmit(event)
+
+ if (onSubmitWithFields) onSubmitWithFields(getFormData(event), event)
+ },
+ }
+}
+
+export { getSubmitProps }
diff --git a/src/components/grid/components/grid.tsx b/src/components/grid/components/grid.tsx
new file mode 100644
index 0000000..be64689
--- /dev/null
+++ b/src/components/grid/components/grid.tsx
@@ -0,0 +1,19 @@
+import { polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { GridProps, GridRef } from '@/components/grid/types/grid'
+import { getMergedStyles } from '@/components/grid/utils/get-merged-styles'
+
+const Grid = React.forwardRef((props, ref) => {
+ const { asChild, className, style, cols, autoFit, autoFill, ...restProps } = props
+
+ const { mergedClassName, mergedStyle } = getMergedStyles(className, style, cols, autoFit, autoFill)
+
+ const Component = polymorphic(asChild, 'div')
+
+ return
+})
+
+Grid.displayName = 'Grid'
+
+export { Grid }
diff --git a/src/components/grid/index.ts b/src/components/grid/index.ts
new file mode 100644
index 0000000..54f101e
--- /dev/null
+++ b/src/components/grid/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/grid/components/grid'
+export * from '@/components/grid/types/grid'
diff --git a/src/components/grid/types/grid.ts b/src/components/grid/types/grid.ts
new file mode 100644
index 0000000..5554490
--- /dev/null
+++ b/src/components/grid/types/grid.ts
@@ -0,0 +1,24 @@
+import { Simplify } from '@renderui/types'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type Without = { [P in Exclude]?: never }
+
+type ExclusiveUnion =
+ T | U extends Record ? (Without & U) | (Without & T) : T | U
+
+type GridRef = React.ElementRef<'div'>
+
+type GridPrimitiveProps = React.ComponentPropsWithoutRef<'div'>
+
+type GridCustomProps = {
+ cols?: string | number
+ autoFit?: number
+ autoFill?: number
+}
+
+type GridAutoProps = ExclusiveUnion<{ autoFit?: number }, { autoFill?: number }>
+
+type GridProps = Simplify
+
+export type { GridProps, GridRef }
diff --git a/src/components/grid/utils/get-merged-styles.ts b/src/components/grid/utils/get-merged-styles.ts
new file mode 100644
index 0000000..6fbebbc
--- /dev/null
+++ b/src/components/grid/utils/get-merged-styles.ts
@@ -0,0 +1,81 @@
+/* eslint-disable max-params */
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { GridProps } from '@/components/grid/types/grid'
+
+const sanitizeCols = (cols: GridProps['cols']) =>
+ Number.isNaN(Number(cols))
+ ? String(cols).replaceAll(/\s/g, '_')
+ : Array.from({ length: Number(cols) })
+ .fill('1fr')
+ .join(' ')
+
+function getColsStyle(
+ style: GridProps['style'],
+ cols: GridProps['cols'],
+ autoFit: GridProps['autoFit'],
+ autoFill: GridProps['autoFill'],
+) {
+ if (autoFit) {
+ return {
+ ...style,
+ '--grid-auto-fit': `${autoFit}px`,
+ } as React.CSSProperties
+ }
+
+ if (autoFill) {
+ return {
+ ...style,
+ '--grid-auto-fill': `${autoFill}px`,
+ } as React.CSSProperties
+ }
+
+ if (cols) {
+ return {
+ ...style,
+ '--grid-cols': sanitizeCols(cols),
+ } as React.CSSProperties
+ }
+
+ return style
+}
+
+function getColsClassName(
+ className: GridProps['className'],
+ cols: GridProps['cols'],
+ autoFit: GridProps['autoFit'],
+ autoFill: GridProps['autoFill'],
+) {
+ if (autoFit) {
+ return 'grid-cols-[repeat(auto-fit,minmax(min(100%,var(--grid-auto-fit)),1fr))]'
+ }
+
+ if (autoFill) {
+ return 'grid-cols-[repeat(auto-fill,minmax(min(100%,var(--grid-auto-fill)),1fr))]'
+ }
+
+ if (cols) {
+ return 'grid-cols-[var(--grid-cols)]'
+ }
+
+ return className
+}
+
+function getMergedStyles(
+ className: GridProps['className'],
+ style: GridProps['style'],
+ cols: GridProps['cols'],
+ autoFit: GridProps['autoFit'],
+ autoFill: GridProps['autoFill'],
+) {
+ const autoClassName = getColsClassName(className, cols, autoFit, autoFill)
+ const autoStyle = getColsStyle(style, cols, autoFit, autoFill)
+
+ return {
+ mergedClassName: cn('render-ui-grid grid', autoClassName, className),
+ mergedStyle: autoStyle,
+ }
+}
+
+export { getMergedStyles }
diff --git a/src/components/heading/classes/heading-classes.ts b/src/components/heading/classes/heading-classes.ts
new file mode 100644
index 0000000..10a44de
--- /dev/null
+++ b/src/components/heading/classes/heading-classes.ts
@@ -0,0 +1,27 @@
+import { cva } from '@renderui/utils'
+
+import { letterSpacingVariants } from '@/components/_shared/variants/letter-spacing'
+import { textBreakVariants } from '@/components/_shared/variants/text-break'
+import { textOverflowVariants } from '@/components/_shared/variants/text-overflow'
+import { textShadowVariants } from '@/components/_shared/variants/text-shadow'
+import { textSizeVariants } from '@/components/_shared/variants/text-size'
+
+const headingClasses = cva('render-ui-heading relative -left-px box-border text-mode-contrast', {
+ variants: {
+ as: {
+ h1: ['text-4xl', 'font-bold', 'tracking-tight'],
+ h2: ['text-3xl', 'font-bold', 'tracking-tight'],
+ h3: ['text-2xl', 'font-bold', 'tracking-tight'],
+ h4: ['text-xl', 'font-bold', 'tracking-tight'],
+ h5: ['text-lg', 'font-bold', 'tracking-tight'],
+ h6: ['text-base', 'font-bold', 'tracking-tight'],
+ },
+ size: textSizeVariants,
+ textBreak: textBreakVariants,
+ shadow: textShadowVariants,
+ overflow: textOverflowVariants,
+ letterSpacing: letterSpacingVariants,
+ },
+})
+
+export { headingClasses }
diff --git a/src/components/heading/components/heading.tsx b/src/components/heading/components/heading.tsx
new file mode 100644
index 0000000..9949f56
--- /dev/null
+++ b/src/components/heading/components/heading.tsx
@@ -0,0 +1,46 @@
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { headingClasses } from '@/components/heading/classes/heading-classes'
+import { HeadingProps, HeadingRef } from '@/components/heading/types/heading'
+
+const Heading = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ children,
+ className,
+ size,
+ overflow,
+ shadow,
+ letterSpacing,
+ textBreak,
+ as = 'h3',
+ ...restProps
+ } = props
+
+ const Component = polymorphic(asChild, as)
+
+ return (
+
+ {children}
+
+ )
+})
+
+Heading.displayName = 'Heading'
+
+export { Heading }
diff --git a/src/components/heading/index.ts b/src/components/heading/index.ts
new file mode 100644
index 0000000..ea27e0d
--- /dev/null
+++ b/src/components/heading/index.ts
@@ -0,0 +1,3 @@
+export * from '@/components/heading/classes/heading-classes'
+export * from '@/components/heading/components/heading'
+export * from '@/components/heading/types/heading'
diff --git a/src/components/heading/types/heading.ts b/src/components/heading/types/heading.ts
new file mode 100644
index 0000000..991ab95
--- /dev/null
+++ b/src/components/heading/types/heading.ts
@@ -0,0 +1,22 @@
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+import { NonNullableVariantProps } from '@/components/_shared/types/variants'
+import { headingClasses } from '@/components/heading/classes/heading-classes'
+
+type HeadingRef = React.ElementRef<'h1'>
+
+type HeadingPrimitiveProps = React.ComponentPropsWithoutRef<'h1'>
+
+type HeadingCustomProps = {
+ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
+}
+
+type HeadingVariantProps = NonNullableVariantProps
+
+type HeadingProps = Simplify<
+ HeadingPrimitiveProps & HeadingCustomProps & HeadingVariantProps & AsChildProp
+>
+
+export type { HeadingProps, HeadingRef }
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..1e8b7df
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,47 @@
+export * from '@/components/accordion'
+export * from '@/components/aria'
+export * from '@/components/aspect-ratio'
+export * from '@/components/box'
+export * from '@/components/button'
+export * from '@/components/card'
+export * from '@/components/checkbox'
+export * from '@/components/collapsible'
+export * from '@/components/combobox'
+export * from '@/components/command'
+export * from '@/components/container'
+export * from '@/components/dialog'
+export * from '@/components/flex'
+export * from '@/components/form'
+export * from '@/components/grid'
+export * from '@/components/heading'
+export * from '@/components/kbd'
+export * from '@/components/label'
+export * from '@/components/link'
+export * from '@/components/loader'
+export * from '@/components/navigation-menu'
+export * from '@/components/number-input'
+export * from '@/components/overlay'
+export * from '@/components/popover'
+export * from '@/components/portal'
+export * from '@/components/progress'
+export * from '@/components/radio-group'
+export * from '@/components/ripple'
+export * from '@/components/scroll-area'
+export * from '@/components/select'
+export * from '@/components/separator'
+export * from '@/components/sheet'
+export * from '@/components/skeleton'
+export * from '@/components/slot'
+export * from '@/components/sub-layer'
+export * from '@/components/switch'
+export * from '@/components/tabs'
+export * from '@/components/text'
+export * from '@/components/text-area'
+export * from '@/components/text-input'
+export * from '@/components/toast'
+export * from '@/components/toast'
+export * from '@/components/toggle'
+export * from '@/components/toggle-group'
+export * from '@/components/toggle-group'
+export * from '@/components/tooltip'
+export * from '@/components/visually-hidden'
\ No newline at end of file
diff --git a/src/components/kbd/components/kbd.tsx b/src/components/kbd/components/kbd.tsx
new file mode 100644
index 0000000..d6540a4
--- /dev/null
+++ b/src/components/kbd/components/kbd.tsx
@@ -0,0 +1,40 @@
+'use client'
+
+import { useKeyboardHotkey } from '@renderui/hooks'
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_KBD_CLASSNAME } from '@/components/kbd/constants/constants'
+import { KbdProps, KbdRef } from '@/components/kbd/types/kbd'
+
+const Kbd = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ children,
+ className,
+ keyCombination,
+ keyCombinationOptions,
+ onKeyCombinationMatch,
+ isKeyCombinationCasingIgnored = true,
+ ...restProps
+ } = props
+
+ useKeyboardHotkey({
+ keyCombination,
+ keyCombinationOptions,
+ onKeyCombinationMatch,
+ isKeyCombinationCasingIgnored,
+ })
+
+ const Component = polymorphic(asChild, 'kbd')
+
+ return (
+
+ {children}
+
+ )
+})
+
+Kbd.displayName = 'Kbd'
+
+export { Kbd }
diff --git a/src/components/kbd/constants/constants.ts b/src/components/kbd/constants/constants.ts
new file mode 100644
index 0000000..f42302d
--- /dev/null
+++ b/src/components/kbd/constants/constants.ts
@@ -0,0 +1,10 @@
+const DEFAULT_HOTKEY_OPTIONS = {
+ preventDefault: true,
+} as const
+
+const DEFAULT_KBD_CLASSNAME =
+ 'render-ui-kbd flex justify-center items-center gap-1 text-xs bg-mode-accent font-[unset] font-medium rounded-md px-2 py-1.5 duration-fast transition-[background-color,box-shadow] shadow-[inset_0_-0.05em_0.5em_#00005506,inset_0_0.05em_hsla(0,0%,100%,0.95),inset_0_0.25em_0.5em_#00005506,inset_0_-0.05em_#00002f26,0_0_0_0.05em_#0009321f,0_0.08em_0.17em_#00062e32] dark:shadow-[inset_0_-0.05em_0.5em_#ddeaf814,inset_0_0.05em_#f1f7feb5,inset_0_0.25em_0.5em_#d8f4f609,inset_0_-0.1em_rgba(0,0,0,0.9),0_0_0_0.075em_#d9edff40,0_0.08em_0.17em_rgba(0,0,0,0.95)]'
+
+const DEFAULT_KBD_ICON_CLASSNAME = 'block h-4 w-4 fill-none stroke-current stroke-[1px]'
+
+export { DEFAULT_HOTKEY_OPTIONS, DEFAULT_KBD_CLASSNAME, DEFAULT_KBD_ICON_CLASSNAME }
diff --git a/src/components/kbd/index.ts b/src/components/kbd/index.ts
new file mode 100644
index 0000000..09a9269
--- /dev/null
+++ b/src/components/kbd/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/kbd/components/kbd'
+export * from '@/components/kbd/types/kbd'
diff --git a/src/components/kbd/types/kbd.ts b/src/components/kbd/types/kbd.ts
new file mode 100644
index 0000000..d046285
--- /dev/null
+++ b/src/components/kbd/types/kbd.ts
@@ -0,0 +1,22 @@
+import { useKeyboardHotkey } from '@renderui/hooks'
+import { Simplify } from '@renderui/types'
+
+type KbdRef = HTMLElement
+
+type KbdHTMLProps = React.ComponentPropsWithoutRef<'kbd'>
+
+type KbdRenderUIProps = {
+ asChild?: boolean
+ hasIcon?: boolean
+ iconPosition?: 'start' | 'end'
+ keyCombination?: Parameters[0]['keyCombination']
+ keyCombinationOptions?: Parameters[0]['keyCombinationOptions']
+ isKeyCombinationCasingIgnored?: Parameters<
+ typeof useKeyboardHotkey
+ >[0]['isKeyCombinationCasingIgnored']
+ onKeyCombinationMatch?: Parameters[0]['onKeyCombinationMatch']
+}
+
+type KbdProps = Simplify
+
+export type { KbdProps, KbdRef }
diff --git a/src/components/label/components/label.tsx b/src/components/label/components/label.tsx
new file mode 100644
index 0000000..77e04b6
--- /dev/null
+++ b/src/components/label/components/label.tsx
@@ -0,0 +1,27 @@
+'use client'
+
+import { chain, cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_LABEL_CLASSNAME } from '@/components/label/constants/constants'
+import {LabelProps, LabelRef} from '@/components/label/types/label'
+import { handlePreventDoubleClickTextSelection } from '@/components/label/utils/handle-prevent-double-click-text-selection'
+
+const Label = React.forwardRef((props, ref) => {
+ const { asChild, className, onMouseDown, ...restProps } = props
+
+ const Component = polymorphic(asChild, 'label')
+
+ return (
+
+ )
+})
+
+Label.displayName = 'Label'
+
+export { Label }
diff --git a/src/components/label/constants/constants.ts b/src/components/label/constants/constants.ts
new file mode 100644
index 0000000..cb97c5e
--- /dev/null
+++ b/src/components/label/constants/constants.ts
@@ -0,0 +1,3 @@
+const DEFAULT_LABEL_CLASSNAME = 'render-ui-label text-base font-medium'
+
+export { DEFAULT_LABEL_CLASSNAME }
diff --git a/src/components/label/index.ts b/src/components/label/index.ts
new file mode 100644
index 0000000..b6d0dec
--- /dev/null
+++ b/src/components/label/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/label/components/label'
+export * from '@/components/label/types/label'
diff --git a/src/components/label/types/label.ts b/src/components/label/types/label.ts
new file mode 100644
index 0000000..e769705
--- /dev/null
+++ b/src/components/label/types/label.ts
@@ -0,0 +1,9 @@
+import { Simplify } from '@renderui/types'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+
+type LabelRef = HTMLLabelElement
+
+type LabelProps = Simplify & AsChildProp>
+
+export type { LabelProps, LabelRef }
diff --git a/src/components/label/utils/handle-prevent-double-click-text-selection.ts b/src/components/label/utils/handle-prevent-double-click-text-selection.ts
new file mode 100644
index 0000000..edc4e25
--- /dev/null
+++ b/src/components/label/utils/handle-prevent-double-click-text-selection.ts
@@ -0,0 +1,9 @@
+import { ONE } from '@renderui/constants'
+
+function handlePreventDoubleClickTextSelection(
+ event: React.MouseEvent,
+) {
+ if (!event.defaultPrevented && event.detail > ONE) event.preventDefault()
+}
+
+export { handlePreventDoubleClickTextSelection }
diff --git a/src/components/link/classes/link-classes.ts b/src/components/link/classes/link-classes.ts
new file mode 100644
index 0000000..459b329
--- /dev/null
+++ b/src/components/link/classes/link-classes.ts
@@ -0,0 +1,20 @@
+import { cva } from 'class-variance-authority'
+
+const linkClasses = cva(
+ 'render-ui-link tap-highlight-transparent appearence-none m-0 box-border inline-flex cursor-pointer items-center bg-transparent p-0 text-base text-primary underline-offset-2 outline-none ring-ring-color ring-offset-background transition-[color,box-shadow] duration-fast focus-visible:ring-[2px] focus-visible:ring-offset-offset active:text-primary/80',
+ {
+ variants: {
+ underline: {
+ 'hover': 'hover:underline',
+ 'active': 'active:underline',
+ 'focus': 'focus:underline',
+ 'focus-visible': 'focus-visible:underline',
+ },
+ },
+ defaultVariants: {
+ underline: 'hover',
+ },
+ },
+)
+
+export { linkClasses }
diff --git a/src/components/link/index.ts b/src/components/link/index.ts
new file mode 100644
index 0000000..374b002
--- /dev/null
+++ b/src/components/link/index.ts
@@ -0,0 +1 @@
+export * from '@/components/link/classes/link-classes'
diff --git a/src/components/loader/classes/loader-classes.ts b/src/components/loader/classes/loader-classes.ts
new file mode 100644
index 0000000..64dd9e6
--- /dev/null
+++ b/src/components/loader/classes/loader-classes.ts
@@ -0,0 +1,83 @@
+import { cva } from '@renderui/utils'
+
+const loaderClasses = cva(['render-ui-loader box-border inline-block aspect-square'], {
+ variants: {
+ isPaused: {
+ true: '!animate-none',
+ false: '',
+ },
+ variant: {
+ base: '',
+ half: '',
+ edge: '',
+ ring: '',
+ dots: 'top-[1px] flex h-full items-center gap-0.5',
+ },
+ size: {
+ sm: 'size-4',
+ md: 'size-5',
+ lg: 'size-6',
+ auto: '',
+ },
+ position: {
+ 'relative': '',
+ 'absolute-center': '',
+ 'absolute-start': '',
+ 'absolute-end': '',
+ },
+ },
+ compoundVariants: [
+ {
+ variant: ['base', 'half', 'edge', 'ring', 'dots'],
+ position: ['absolute-center', 'absolute-start', 'absolute-end'],
+ className: 'pointer-events-none absolute',
+ },
+ {
+ position: 'absolute-start',
+ className: 'left-3',
+ },
+ {
+ position: 'absolute-end',
+ className: 'right-3',
+ },
+ {
+ variant: 'dots',
+ position: 'relative',
+ className: 'relative',
+ },
+ {
+ variant: ['base', 'half', 'ring'],
+ className: 'border-[2px]',
+ },
+ {
+ variant: ['edge'],
+ className: 'border-x-[2px] border-b-[2px]',
+ },
+ {
+ variant: ['base', 'half', 'edge', 'ring'],
+ className: 'rounded-full',
+ },
+ {
+ variant: 'base',
+ className: 'border-x-current border-b-current border-t-transparent',
+ },
+ {
+ variant: 'edge',
+ className: 'border-y-transparent border-l-transparent border-r-current',
+ },
+ {
+ variant: 'half',
+ className: 'border-b-transparent border-l-current border-r-transparent border-t-current',
+ },
+ {
+ variant: ['ring'],
+ className: 'border-x-mode-accent border-b-mode-accent border-t-current',
+ },
+ {
+ variant: ['base', 'half', 'edge', 'ring'],
+ className: 'animate-[spin_700ms_linear_infinite]',
+ },
+ ],
+})
+
+export { loaderClasses }
diff --git a/src/components/loader/components/loader-dot.tsx b/src/components/loader/components/loader-dot.tsx
new file mode 100644
index 0000000..d15936c
--- /dev/null
+++ b/src/components/loader/components/loader-dot.tsx
@@ -0,0 +1,44 @@
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_LOADER_DOT_CLASSNAME,
+ LOADER_DOT_PAUSED_CLASSNAME,
+} from '@/components/loader/constants/constants'
+import { LoaderDotProps } from '@/components/loader/types/loader-dot'
+
+const LoaderDot = (props: LoaderDotProps) => {
+ const { isPaused, className, style, element } = props
+
+ const getMergedStyle = () => {
+ if (element === 'middle') {
+ return {
+ animationDelay: '200ms',
+ ...style,
+ }
+ }
+
+ if (element === 'end') {
+ return {
+ animationDelay: '400ms',
+ ...style,
+ }
+ }
+
+ return style
+ }
+
+ return (
+
+ )
+}
+
+export { LoaderDot }
diff --git a/src/components/loader/components/loader.tsx b/src/components/loader/components/loader.tsx
new file mode 100644
index 0000000..fffdd15
--- /dev/null
+++ b/src/components/loader/components/loader.tsx
@@ -0,0 +1,51 @@
+import { cn, polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { loaderClasses } from '@/components/loader/classes/loader-classes'
+import { LoaderDot } from '@/components/loader/components/loader-dot'
+import { LOADER_DOTS } from '@/components/loader/constants/constants'
+import { LoaderProps, LoaderRef } from '@/components/loader/types/loader'
+
+const Loader = React.forwardRef((props, ref) => {
+ const {
+ asChild,
+ isPaused,
+ position = 'relative',
+ variant = 'half',
+ size = 'sm',
+ className,
+ ...restProps
+ } = props
+
+ const Component = polymorphic(asChild, 'span')
+
+ return (
+
+ {Boolean(variant === 'dots') && (
+ <>
+ {LOADER_DOTS.map((element) => (
+
+ ))}
+ >
+ )}
+
+ )
+})
+
+Loader.displayName = 'Loader'
+
+export { Loader }
diff --git a/src/components/loader/constants/constants.ts b/src/components/loader/constants/constants.ts
new file mode 100644
index 0000000..62eebe5
--- /dev/null
+++ b/src/components/loader/constants/constants.ts
@@ -0,0 +1,8 @@
+const DEFAULT_LOADER_DOT_CLASSNAME =
+ 'render-ui-loader-dot box-border inline-block size-1 animate-[blink_1400ms_linear_infinite] rounded-full bg-current'
+
+const LOADER_DOT_PAUSED_CLASSNAME = '!animate-none'
+
+const LOADER_DOTS = ['start', 'middle', 'end'] as const
+
+export { DEFAULT_LOADER_DOT_CLASSNAME, LOADER_DOT_PAUSED_CLASSNAME, LOADER_DOTS }
diff --git a/src/components/loader/index.ts b/src/components/loader/index.ts
new file mode 100644
index 0000000..eadefe6
--- /dev/null
+++ b/src/components/loader/index.ts
@@ -0,0 +1,4 @@
+export * from '@/components/loader/classes/loader-classes'
+export * from '@/components/loader/components/loader'
+export * from '@/components/loader/types/loader'
+export * from '@/components/loader/types/loader-dot'
diff --git a/src/components/loader/types/loader-dot.ts b/src/components/loader/types/loader-dot.ts
new file mode 100644
index 0000000..87f81cf
--- /dev/null
+++ b/src/components/loader/types/loader-dot.ts
@@ -0,0 +1,12 @@
+import { Simplify } from '@renderui/types'
+
+type LoaderDotPrimitiveProps = React.ComponentPropsWithoutRef<'span'>
+
+type LoaderDotCustomProps = {
+ isPaused: boolean | undefined
+ element?: 'start' | 'middle' | 'end'
+}
+
+type LoaderDotProps = Simplify
+
+export type { LoaderDotProps }
diff --git a/src/components/loader/types/loader.ts b/src/components/loader/types/loader.ts
new file mode 100644
index 0000000..e14a4b0
--- /dev/null
+++ b/src/components/loader/types/loader.ts
@@ -0,0 +1,15 @@
+import { Simplify } from '@renderui/types'
+
+import { AsChildProp } from '@/components/_shared/types/as-child'
+import { NonNullableVariantProps } from '@/components/_shared/types/variants'
+import { loaderClasses } from '@/components/loader/classes/loader-classes'
+
+type LoaderRef = React.ElementRef<'span'>
+
+type LoaderPrimitiveProps = React.ComponentPropsWithoutRef<'span'>
+
+type LodaerRenderUIProps = NonNullableVariantProps
+
+type LoaderProps = Simplify
+
+export type { LoaderProps, LoaderRef }
diff --git a/src/components/navigation-menu/components/navigation-menu-content.tsx b/src/components/navigation-menu/components/navigation-menu-content.tsx
new file mode 100644
index 0000000..892ee1c
--- /dev/null
+++ b/src/components/navigation-menu/components/navigation-menu-content.tsx
@@ -0,0 +1,28 @@
+import { NavigationMenuContent as NavigationMenuContentPrimitive } from '@radix-ui/react-navigation-menu'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_NAVIGATION_MENU_CONTENT_CLASSNAME } from '@/components/navigation-menu/constants/constants'
+import {
+ NavigationMenuContentProps,
+ NavigationMenuContentRef,
+} from '@/components/navigation-menu/types/navigation-menu-content'
+
+const NavigationMenuContent = React.forwardRef<
+ NavigationMenuContentRef,
+ NavigationMenuContentProps
+>((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+NavigationMenuContent.displayName = 'NavigationMenuContent'
+
+export { NavigationMenuContent }
diff --git a/src/components/navigation-menu/components/navigation-menu-item.tsx b/src/components/navigation-menu/components/navigation-menu-item.tsx
new file mode 100644
index 0000000..a073ca0
--- /dev/null
+++ b/src/components/navigation-menu/components/navigation-menu-item.tsx
@@ -0,0 +1,27 @@
+import { NavigationMenuItem as NavigationMenuItemPrimitive } from '@radix-ui/react-navigation-menu'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_NAVIGATION_MENU_ITEM_CLASSNAME } from '@/components/navigation-menu/constants/constants'
+import {
+ NavigationMenuItemProps,
+ NavigationMenuItemRef,
+} from '@/components/navigation-menu/types/navigation-menu-item'
+
+const NavigationMenuItem = React.forwardRef(
+ (props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+ },
+)
+
+NavigationMenuItem.displayName = 'NavigationMenuItem'
+
+export { NavigationMenuItem }
diff --git a/src/components/navigation-menu/components/navigation-menu-link.tsx b/src/components/navigation-menu/components/navigation-menu-link.tsx
new file mode 100644
index 0000000..e379645
--- /dev/null
+++ b/src/components/navigation-menu/components/navigation-menu-link.tsx
@@ -0,0 +1,33 @@
+'use client'
+
+import { NavigationMenuLink as NavigationMenuLinkPrimitive } from '@radix-ui/react-navigation-menu'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button'
+import { DEFAULT_NAVIGATION_MENU_LINK_CLASSNAME } from '@/components/navigation-menu/constants/constants'
+import {
+ NavigationMenuLinkProps,
+ NavigationMenuLinkRef,
+} from '@/components/navigation-menu/types/navigation-menu-link'
+
+const NavigationMenuLink = React.forwardRef(
+ (props, ref) => {
+ const { onSelect, className, active, variant = 'plain', ...restProps } = props
+
+ return (
+
+
+
+ )
+ },
+)
+
+NavigationMenuLink.displayName = 'NavigationMenuLink'
+
+export { NavigationMenuLink }
diff --git a/src/components/navigation-menu/components/navigation-menu-trigger.tsx b/src/components/navigation-menu/components/navigation-menu-trigger.tsx
new file mode 100644
index 0000000..a52caf8
--- /dev/null
+++ b/src/components/navigation-menu/components/navigation-menu-trigger.tsx
@@ -0,0 +1,70 @@
+'use client'
+
+import { ChevronDownIcon } from '@radix-ui/react-icons'
+import { NavigationMenuTrigger as NavigationMenuTriggerPrimitive } from '@radix-ui/react-navigation-menu'
+import { chain, cn, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button'
+import {
+ DEFAULT_NAVIGATION_MENU_TRIGGER_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_TRIGGER_INDICATOR_CLASSNAME,
+} from '@/components/navigation-menu/constants/constants'
+import {
+ NavigationMenuTriggerProps,
+ NavigationMenuTriggerRef,
+} from '@/components/navigation-menu/types/navigation-menu-trigger'
+
+const NavigationMenuTrigger = React.forwardRef<
+ NavigationMenuTriggerRef,
+ NavigationMenuTriggerProps
+>((props, ref) => {
+ const {
+ className,
+ children,
+ indicator,
+ indicatorProps,
+ onMouseEnter,
+ onMouseLeave,
+ hasIndicator = true,
+ ...restProps
+ } = props
+
+ // aria hover event not always firing in combination with radix asChild, track manually with native event
+ const [isHovered, setIsHovered] = React.useState(false)
+
+ const { className: indicatorClassName, ...restIndicatorProps } = getOptionalObject(indicatorProps)
+
+ const renderIndicator = () => {
+ if (!hasIndicator) return null
+
+ if (indicator) return indicator
+
+ return (
+
+ )
+ }
+
+ return (
+
+ setIsHovered(true))}
+ onMouseLeave={chain(onMouseLeave, () => setIsHovered(false))}
+ data-hover={isHovered}
+ >
+ {children}
+ {renderIndicator()}
+
+
+ )
+})
+
+NavigationMenuTrigger.displayName = 'NavigationMenuTrigger'
+
+export { NavigationMenuTrigger }
diff --git a/src/components/navigation-menu/components/navigation-menu.tsx b/src/components/navigation-menu/components/navigation-menu.tsx
new file mode 100644
index 0000000..53315ec
--- /dev/null
+++ b/src/components/navigation-menu/components/navigation-menu.tsx
@@ -0,0 +1,94 @@
+'use client'
+
+import {
+ NavigationMenu as NavigationMenuPrimitive,
+ NavigationMenuIndicator as NavigationMenuIndicatorPrimitive,
+ NavigationMenuList as NavigationMenuListPrimitive,
+ NavigationMenuViewport as NavigationMenuViewportPrimitive,
+} from '@radix-ui/react-navigation-menu'
+import { cn, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_NAVIGATION_MENU_ARROW_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_INDICATOR_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_LIST_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_VIEWPORT_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_VIEWPORT_CONTAINER_CLASSNAME,
+} from '@/components/navigation-menu/constants/constants'
+import {
+ NavigationMenuProps,
+ NavigationMenuRef,
+} from '@/components/navigation-menu/types/navigation-menu'
+
+const NavigationMenu = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ viewportProps,
+ viewportContainerProps,
+ listProps,
+ indicatorProps,
+ indicatorArrowProps,
+ viewportRef,
+ viewportContainerRef,
+ listRef,
+ indicatorRef,
+ indicatorArrowRef,
+ ...restProps
+ } = props
+
+ const { className: indicatorClassName, ...restIndicatorProps } = getOptionalObject(indicatorProps)
+ const { className: indicatorArrowClassName, ...restArrowProps } =
+ getOptionalObject(indicatorArrowProps)
+ const { className: listClassName, ...restListProps } = getOptionalObject(listProps)
+ const { className: viewportClassName, ...restViewportProps } = getOptionalObject(viewportProps)
+ const { className: viewportContainerClassName, ...restViewportContainerProps } =
+ getOptionalObject(viewportContainerProps)
+
+ return (
+
+
+ {children}
+
+
+
+
+
+
+
+
+ )
+})
+
+NavigationMenu.displayName = 'NavigationMenu'
+
+export { NavigationMenu }
diff --git a/src/components/navigation-menu/constants/constants.ts b/src/components/navigation-menu/constants/constants.ts
new file mode 100644
index 0000000..17686c8
--- /dev/null
+++ b/src/components/navigation-menu/constants/constants.ts
@@ -0,0 +1,44 @@
+const DEFAULT_NAVIGATION_MENU_CLASSNAME =
+ 'render-ui-navigation-menu relative z-[1] flex w-full justify-center'
+
+const DEFAULT_NAVIGATION_MENU_LIST_CLASSNAME =
+ 'render-ui-navigation-menu-list m-0 flex list-none justify-center rounded-md p-1'
+
+const DEFAULT_NAVIGATION_MENU_INDICATOR_CLASSNAME =
+ 'render-ui-navigation-menu-indicator top-full z-[1] flex h-[10px] items-end justify-center overflow-hidden transition-[width,transform_250ms_ease] data-[state=hidden]:animate-navigation-menu-fade-out data-[state=visible]:animate-navigation-menu-fade-in'
+
+const DEFAULT_NAVIGATION_MENU_ARROW_CLASSNAME =
+ 'render-ui-navigation-menu-arrow relative top-[70%] h-2.5 w-2.5 rotate-45 rounded-tl-[2px] bg-mode-accent'
+
+const DEFAULT_NAVIGATION_MENU_VIEWPORT_CONTAINER_CLASSNAME =
+ 'render-ui-navigation-menu-viewport-container absolute z-[1] left-0 top-full flex w-full justify-center perspective-[2000px]'
+
+const DEFAULT_NAVIGATION_MENU_VIEWPORT_CLASSNAME =
+ 'render-ui-navigation-menu-viewport relative mt-2.5 h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-md bg-mode shadow-even-xl [&]:shadow-mode-contrast/20 dark:border-[1px] dark:border-mode-accent [&]:dark:shadow-mode-contrast/[1%] transition-all duration-fast data-[state=closed]:animate-navigation-menu-scale-out data-[state=open]:animate-navigation-menu-scale-in sm:w-[var(--radix-navigation-menu-viewport-width)]'
+
+const DEFAULT_NAVIGATION_MENU_ITEM_CLASSNAME = 'render-ui-navigation-menu-item'
+
+const DEFAULT_NAVIGATION_MENU_TRIGGER_CLASSNAME =
+ 'render-ui-navigation-menu-trigger group flex select-none gap-0.5 rounded'
+
+const DEFAULT_NAVIGATION_MENU_TRIGGER_INDICATOR_CLASSNAME =
+ 'render-ui-navigation-menu-trigger-indicator relative top-px ml-1 h-3 w-3 group-data-[state=open]:rotate-180 transition-transform duration-fast'
+
+const DEFAULT_NAVIGATION_MENU_CONTENT_CLASSNAME =
+ 'render-ui-navigation-menu-content absolute left-0 top-0 w-full data-[motion=from-end]:animate-navigation-menu-enter-from-right data-[motion=from-start]:animate-navigation-menu-enter-from-left data-[motion=to-end]:animate-navigation-menu-exit-to-right data-[motion=to-start]:animate-navigation-menu-exit-to-left sm:w-auto min-w-[200px] min-h-[200px] p-4'
+
+const DEFAULT_NAVIGATION_MENU_LINK_CLASSNAME = 'render-ui-navigation-menu-link rounded'
+
+export {
+ DEFAULT_NAVIGATION_MENU_ARROW_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_CONTENT_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_INDICATOR_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_ITEM_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_LINK_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_LIST_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_TRIGGER_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_TRIGGER_INDICATOR_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_VIEWPORT_CLASSNAME,
+ DEFAULT_NAVIGATION_MENU_VIEWPORT_CONTAINER_CLASSNAME,
+}
diff --git a/src/components/navigation-menu/index.ts b/src/components/navigation-menu/index.ts
new file mode 100644
index 0000000..fd28403
--- /dev/null
+++ b/src/components/navigation-menu/index.ts
@@ -0,0 +1,10 @@
+export * from '@/components/navigation-menu/components/navigation-menu'
+export * from '@/components/navigation-menu/components/navigation-menu-content'
+export * from '@/components/navigation-menu/components/navigation-menu-item'
+export * from '@/components/navigation-menu/components/navigation-menu-link'
+export * from '@/components/navigation-menu/components/navigation-menu-trigger'
+export * from '@/components/navigation-menu/types/navigation-menu'
+export * from '@/components/navigation-menu/types/navigation-menu-content'
+export * from '@/components/navigation-menu/types/navigation-menu-item'
+export * from '@/components/navigation-menu/types/navigation-menu-link'
+export * from '@/components/navigation-menu/types/navigation-menu-trigger'
\ No newline at end of file
diff --git a/src/components/navigation-menu/types/navigation-menu-content.ts b/src/components/navigation-menu/types/navigation-menu-content.ts
new file mode 100644
index 0000000..1a3aed0
--- /dev/null
+++ b/src/components/navigation-menu/types/navigation-menu-content.ts
@@ -0,0 +1,10 @@
+import { NavigationMenuContent as NavigationMenuContentPrimitive } from '@radix-ui/react-navigation-menu'
+import React from 'react'
+
+type NavigationMenuContentPrimitiveType = typeof NavigationMenuContentPrimitive
+
+type NavigationMenuContentRef = React.ElementRef
+
+type NavigationMenuContentProps = React.ComponentPropsWithoutRef
+
+export type { NavigationMenuContentProps, NavigationMenuContentRef }
diff --git a/src/components/navigation-menu/types/navigation-menu-item.ts b/src/components/navigation-menu/types/navigation-menu-item.ts
new file mode 100644
index 0000000..2f4b911
--- /dev/null
+++ b/src/components/navigation-menu/types/navigation-menu-item.ts
@@ -0,0 +1,10 @@
+import { NavigationMenuItem as NavigationMenuItemPrimitive } from '@radix-ui/react-navigation-menu'
+import React from 'react'
+
+type NavigationMenuItemPrimitiveType = typeof NavigationMenuItemPrimitive
+
+type NavigationMenuItemRef = React.ElementRef
+
+type NavigationMenuItemProps = React.ComponentPropsWithoutRef
+
+export type { NavigationMenuItemProps, NavigationMenuItemRef }
diff --git a/src/components/navigation-menu/types/navigation-menu-link.ts b/src/components/navigation-menu/types/navigation-menu-link.ts
new file mode 100644
index 0000000..531e4c2
--- /dev/null
+++ b/src/components/navigation-menu/types/navigation-menu-link.ts
@@ -0,0 +1,18 @@
+import { NavigationMenuLink as NavigationMenuLinkPrimitive } from '@radix-ui/react-navigation-menu'
+import { Simplify } from '@renderui/types'
+
+import { ButtonProps, ButtonRef } from '@/components/button'
+
+type NavigationMenuLinkPrimitiveProps = React.ComponentPropsWithoutRef<
+ typeof NavigationMenuLinkPrimitive
+>
+
+type NavigationMenuLinkRef = ButtonRef
+
+type NavigationMenuLinkButtonProps = ButtonProps
+
+type NavigationMenuLinkLinkProps = Pick
+
+type NavigationMenuLinkProps = Simplify
+
+export type { NavigationMenuLinkProps, NavigationMenuLinkRef }
diff --git a/src/components/navigation-menu/types/navigation-menu-trigger.ts b/src/components/navigation-menu/types/navigation-menu-trigger.ts
new file mode 100644
index 0000000..075635a
--- /dev/null
+++ b/src/components/navigation-menu/types/navigation-menu-trigger.ts
@@ -0,0 +1,23 @@
+import { ChevronDownIcon } from '@radix-ui/react-icons'
+import { NavigationMenuTrigger as NavigationMenuTriggerPrimitive } from '@radix-ui/react-navigation-menu'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+type NavigationMenuTriggerPrimitiveType = typeof NavigationMenuTriggerPrimitive
+
+type NavigationMenuTriggerRef = React.ElementRef
+
+type NavigationMenuTriggerPrimitiveProps =
+ React.ComponentPropsWithoutRef
+
+type NavigationMenuTriggerRenderUIProps = {
+ indicator?: React.ReactNode
+ indicatorProps?: React.ComponentPropsWithoutRef
+ hasIndicator?: boolean
+}
+
+type NavigationMenuTriggerProps = Simplify<
+ NavigationMenuTriggerPrimitiveProps & NavigationMenuTriggerRenderUIProps
+>
+
+export type { NavigationMenuTriggerProps, NavigationMenuTriggerRef }
diff --git a/src/components/navigation-menu/types/navigation-menu.ts b/src/components/navigation-menu/types/navigation-menu.ts
new file mode 100644
index 0000000..846b0f4
--- /dev/null
+++ b/src/components/navigation-menu/types/navigation-menu.ts
@@ -0,0 +1,60 @@
+import {
+ NavigationMenu as NavigationMenuPrimitive,
+ NavigationMenuIndicator as NavigationMenuIndicatorPrimitive,
+ NavigationMenuList as NavigationMenuListPrimitive,
+ NavigationMenuViewport as NavigationMenuViewportPrimitive,
+} from '@radix-ui/react-navigation-menu'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+type NavigationMenuPrimitiveType = typeof NavigationMenuPrimitive
+
+type NavigationMenuIndicatorPrimitiveType = typeof NavigationMenuIndicatorPrimitive
+
+type NavigationMenuListPrimitiveType = typeof NavigationMenuListPrimitive
+
+type NavigationMenuViewportPrimitiveType = typeof NavigationMenuViewportPrimitive
+
+type NavigationMenuRef = React.ElementRef
+
+type NavigationMenuIndicatorRef = React.ElementRef
+
+type NavigationMenuIndicatorArrowRef = HTMLDivElement
+
+type NavigationMenuListRef = React.ElementRef
+
+type NavigationMenuViewportContainerRef = HTMLDivElement
+
+type NavigationMenuViewportRef = React.ElementRef
+
+type NavigationMenuPrimitiveProps = React.ComponentPropsWithoutRef
+
+type NavigationMenuIndicatorPrimitiveProps =
+ React.ComponentPropsWithoutRef
+
+type NavigationMenuIndicatorArrowProps = React.ComponentPropsWithoutRef<'div'>
+
+type NavigationMenuListPrimitiveProps =
+ React.ComponentPropsWithoutRef
+
+type NavigationMenuViewportContainerProps = React.ComponentPropsWithoutRef<'div'>
+
+type NavigationMenuViewportPrimitiveProps =
+ React.ComponentPropsWithoutRef
+
+type NavigationMenuRenderUIProps = {
+ listProps?: NavigationMenuListPrimitiveProps
+ listRef?: React.Ref
+ viewportContainerProps?: NavigationMenuViewportContainerProps
+ viewportContainerRef?: React.Ref
+ viewportProps?: NavigationMenuViewportPrimitiveProps
+ viewportRef?: React.Ref
+ indicatorProps?: NavigationMenuIndicatorPrimitiveProps
+ indicatorRef?: React.Ref
+ indicatorArrowProps?: NavigationMenuIndicatorArrowProps
+ indicatorArrowRef?: React.Ref
+}
+
+type NavigationMenuProps = Simplify
+
+export type { NavigationMenuProps, NavigationMenuRef }
diff --git a/src/components/number-input/components/number-input.tsx b/src/components/number-input/components/number-input.tsx
new file mode 100644
index 0000000..b507028
--- /dev/null
+++ b/src/components/number-input/components/number-input.tsx
@@ -0,0 +1,50 @@
+'use client'
+
+import { polymorphic } from '@renderui/utils'
+import React from 'react'
+
+import { Aria } from '@/components/aria'
+import { NumberSpinButton } from '@/components/number-input/components/number-spin-button'
+import { useNumberInput } from '@/components/number-input/hooks/use-number-input'
+import { NumberInputProps, NumberInputRef } from '@/components/number-input/types/number-input'
+import { Separator } from '@/components/separator'
+
+const NumberInput = React.forwardRef((props, ref) => {
+ const {
+ inputContainerProps,
+ inputProps,
+ spinButtonContainerProps,
+ incrementButtonProps,
+ decrementButtonProps,
+ separatorProps,
+ utilityProps,
+ } = useNumberInput(props, ref)
+
+ const { startContent, children, endContent } = utilityProps
+
+ const { asChild: inputAsChild, ...restInputProps } = inputProps
+
+ const { asChild: spinButtonContainerAsChild, ...restSpinButtonContainerProps } =
+ spinButtonContainerProps
+
+ const InputComponent = polymorphic(inputAsChild, 'input')
+ const SpinButtonContainerComponent = polymorphic(spinButtonContainerAsChild, 'div')
+
+ return (
+
+ {startContent}
+
+
+
+
+
+
+ {children}
+ {endContent}
+
+ )
+})
+
+NumberInput.displayName = 'NumberInput'
+
+export { NumberInput }
diff --git a/src/components/number-input/components/number-spin-button.tsx b/src/components/number-input/components/number-spin-button.tsx
new file mode 100644
index 0000000..981184b
--- /dev/null
+++ b/src/components/number-input/components/number-spin-button.tsx
@@ -0,0 +1,50 @@
+import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button'
+import {
+ DEFAULT_NUMBER_SPIN_BUTTON_CLASSNAME,
+ DEFAULT_NUMBER_SPIN_BUTTON_ICON_CLASSNAME,
+} from '@/components/number-input/constants/constants'
+import { NumberSpinButtonProps } from '@/components/number-input/types/number-spin-button'
+
+const NumberSpinButton = (props: NumberSpinButtonProps) => {
+ const {
+ children,
+ className,
+ variant = 'plain',
+ action = 'increment',
+ tabIndex = -1,
+ longPressTreshold = 100,
+ isLongPressDisabled = false,
+ preventFocusOnPress = true,
+ hasDefaultPressedStyles = false,
+ ...restProps
+ } = props
+
+ const renderChildren = () => {
+ if (action === 'increment') {
+ return
+ }
+
+ return
+ }
+
+ return (
+
+ {children ?? renderChildren()}
+
+ )
+}
+
+export { NumberSpinButton }
diff --git a/src/components/number-input/constants/constants.ts b/src/components/number-input/constants/constants.ts
new file mode 100644
index 0000000..232ef8b
--- /dev/null
+++ b/src/components/number-input/constants/constants.ts
@@ -0,0 +1,41 @@
+const SPIN_TIMEOUT = 175
+
+const INITIAL_SPIN_TRESHOLD_TIMEOUT = 0
+const ACCELERATED_SPIN_TRESHOLD_TIMEOUT = 500
+const INITIAL_SPIN_HOLD_TIMEOUT = 400
+const ACCELERATED_SPIN_HOLD_TIMEOUT = 100
+const HOLDING_SPIN_TIMEOUT = 25
+
+const FORBIDDEN_INPUT_CHARACTERS = ['e', 'E']
+
+// eslint-disable-next-line security/detect-unsafe-regex
+const NUMERIC_REGEX = /^[+-]?\d*(?:\.\d*)?$/u
+
+const DEFAULT_NUMBER_SPIN_BUTTON_ICON_CLASSNAME = 'pointer-events-none relative w-3 h-3'
+
+const DEFAULT_NUMBER_INPUT_CONTAINER_CLASSNAME = 'render-ui-number-input-container'
+
+const DEFAULT_NUMBER_INPUT_CLASSNAME =
+ 'render-ui-number-input text-sm pl-3 pr-[44px] appearence-none text-mode-contrast bg-transparent outline-none text-elipsis overflow-hidden min-w-[0px] h-full w-full data-[disabled=true]:cursor-[inherit] data-[disabled=true]:pointer-events-none placeholder:text-mode-foreground/50'
+
+const DFEAULT_NUMBER_INPUT_SPIN_BUTTON_CONTAINER_CLASSNAME =
+ 'render-ui-number-input-spin-button-container absolute right-0 top-0 flex h-full flex-col border-l-[1px] border-separator p-0'
+
+const DEFAULT_NUMBER_SPIN_BUTTON_CLASSNAME =
+ 'render-ui-number-input-spin-button flex-1 rounded-none px-2 py-0 data-[hover=true]:bg-mode-accent/50 data-[pressed=true]:bg-mode-accent data-[long-pressed=true]:bg-mode-accent'
+
+export {
+ ACCELERATED_SPIN_HOLD_TIMEOUT,
+ ACCELERATED_SPIN_TRESHOLD_TIMEOUT,
+ DEFAULT_NUMBER_INPUT_CLASSNAME,
+ DEFAULT_NUMBER_INPUT_CONTAINER_CLASSNAME,
+ DEFAULT_NUMBER_SPIN_BUTTON_CLASSNAME,
+ DEFAULT_NUMBER_SPIN_BUTTON_ICON_CLASSNAME,
+ DFEAULT_NUMBER_INPUT_SPIN_BUTTON_CONTAINER_CLASSNAME,
+ FORBIDDEN_INPUT_CHARACTERS,
+ HOLDING_SPIN_TIMEOUT,
+ INITIAL_SPIN_HOLD_TIMEOUT,
+ INITIAL_SPIN_TRESHOLD_TIMEOUT,
+ NUMERIC_REGEX,
+ SPIN_TIMEOUT,
+}
diff --git a/src/components/number-input/hooks/use-number-input.ts b/src/components/number-input/hooks/use-number-input.ts
new file mode 100644
index 0000000..228a414
--- /dev/null
+++ b/src/components/number-input/hooks/use-number-input.ts
@@ -0,0 +1,235 @@
+import { useControllableState, useMergedRef, useOnClickOutside } from '@renderui/hooks'
+import { chain, cn, cx, functionCallOrValue, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import { inputContainerClasses } from '@/components/_shared/classes/input-container-classes'
+import { focusInput } from '@/components/_shared/utils/focus-input'
+import { buttonClasses } from '@/components/button'
+import {
+ DEFAULT_NUMBER_INPUT_CLASSNAME,
+ DEFAULT_NUMBER_INPUT_CONTAINER_CLASSNAME,
+ DFEAULT_NUMBER_INPUT_SPIN_BUTTON_CONTAINER_CLASSNAME,
+} from '@/components/number-input/constants/constants'
+import { useNumberSpin } from '@/components/number-input/hooks/use-number-spin'
+import { getOnChange } from '@/components/number-input/hooks/use-on-change'
+import { NumberInputProps, NumberInputRef } from '@/components/number-input/types/number-input'
+import { getHandleKeyPressCapture } from '@/components/number-input/utils/get-handle-key-press-capture'
+
+function useNumberInput(props: NumberInputProps, ref: React.Ref) {
+ const {
+ min,
+ max,
+ pattern,
+ precision,
+ className,
+ children,
+ startContent,
+ endContent,
+ isDisabled,
+ isReadOnly,
+ isInvalid,
+ isRequired,
+ inputContainerProps,
+ spinButtonContainerProps,
+ incrementButtonProps,
+ decrementButtonProps,
+ separatorProps,
+ onClick,
+ onMouseDown,
+ onKeyDownCapture,
+ onValueChange,
+ onPointerDown,
+ onChange: nativeOnChange,
+ onSpin,
+ onSpinIncrement,
+ onSpinDecrement,
+ value: valueProp,
+ step = '1',
+ size = 'md',
+ defaultValue = '',
+ ...restProps
+ } = props
+
+ const [value, setValue] = useControllableState({
+ prop: valueProp as string,
+ defaultProp: defaultValue as string,
+ onChange: onValueChange,
+ })
+
+ const internalInputRef = React.useRef(null)
+ const mergedRefCallback = useMergedRef([internalInputRef, ref])
+
+ const {
+ increment,
+ incrementWithVariableSpeed,
+ decrement,
+ decrementWithVariableSpeed,
+ stopIncrementing,
+ stopDecrementing,
+ clearIntervals,
+ } = useNumberSpin(
+ {
+ value,
+ min,
+ max,
+ step,
+ pattern,
+ setValue,
+ onSpin,
+ onSpinIncrement,
+ onSpinDecrement,
+ },
+ internalInputRef,
+ )
+
+ useOnClickOutside('pointerdown', internalInputRef, clearIntervals)
+
+ const {
+ className: inputContainerClassName,
+ onPointerDown: inputContainerOnPointerDown,
+ onClick: inputContainerOnClick,
+ isTextInput = true,
+ isFocusWithin = true,
+ ...restInputContainerProps
+ } = getOptionalObject(inputContainerProps)
+
+ const { className: spinButtonContainerClassName, ...restSpinButtonContainerClassName } =
+ getOptionalObject(spinButtonContainerProps)
+
+ const {
+ className: incrementButtonClassName,
+ onPress: incrementButtonOnPress,
+ onLongPress: incrementOnLongPress,
+ onPointerUp: incrementOnPointerUp,
+ onPointerLeave: incrementOnPointerLeave,
+ onPointerCancel: incrementOnPointerCancel,
+ ...restIncrementButtonClassName
+ } = getOptionalObject(incrementButtonProps)
+
+ const {
+ className: decrementButtonClassName,
+ onPress: decrementButtonOnPress,
+ onLongPress: decrementOnLongPress,
+ onPointerUp: decrementOnPointerUp,
+ onPointerLeave: decrementOnPointerLeave,
+ onPointerCancel: decrementOnPointerCancel,
+ ...restDecrementButtonClassName
+ } = getOptionalObject(decrementButtonProps)
+
+ return {
+ inputContainerProps: {
+ isTextInput,
+ isFocusWithin,
+ isDisabled,
+ 'data-disabled': isDisabled,
+ 'data-read-only': isReadOnly,
+ 'data-invalid': isInvalid,
+ 'data-required': isRequired,
+ 'data-slot': 'base',
+ 'className': cx(
+ DEFAULT_NUMBER_INPUT_CONTAINER_CLASSNAME,
+ buttonClasses({
+ variant: 'solid',
+ hasDefaultHoverStyles: false,
+ hasDefaultPressedStyles: false,
+ hasLoaderOnLoading: false,
+ hasLowerOpacityOnLoading: false,
+ }),
+ inputContainerClasses({ size }),
+ inputContainerClassName,
+ ),
+ 'onPointerDown': chain(
+ (event: React.PointerEvent) => event.preventDefault(),
+ inputContainerOnPointerDown,
+ ),
+ 'onClick': chain(() => focusInput(internalInputRef), inputContainerOnClick),
+ ...restInputContainerProps,
+ },
+ inputProps: {
+ pattern,
+ min,
+ max,
+ step,
+ 'ref': mergedRefCallback,
+ 'value': value ?? '',
+ 'role': 'spinbutton',
+ 'inputMode': 'numeric',
+ 'aria-valuemin': min ? Number(min) : undefined,
+ 'aria-valuemax': max ? Number(max) : undefined,
+ 'aria-valuenow': value ? Number(value) : undefined,
+ 'aria-required': isRequired,
+ 'aria-disabled': isDisabled,
+ 'aria-readonly': isReadOnly,
+ 'aria-invalid': isInvalid,
+ 'data-disabled': isDisabled,
+ 'data-read-only': isReadOnly,
+ 'data-invalid': isInvalid,
+ 'data-required': isRequired,
+ 'data-slot': 'input',
+ 'disabled': isDisabled,
+ 'readOnly': isReadOnly,
+ 'className': cn(DEFAULT_NUMBER_INPUT_CLASSNAME, className),
+ 'onChange': getOnChange({ min, max, pattern, precision, setValue, onChange: nativeOnChange }),
+ 'onPointerDown': chain(onPointerDown, (event: React.PointerEvent) =>
+ event.stopPropagation(),
+ ),
+ 'onKeyDownCapture': chain(
+ getHandleKeyPressCapture({
+ min,
+ max,
+ step,
+ pattern,
+ setValue,
+ }),
+ onKeyDownCapture,
+ ),
+ 'onClick': chain(
+ (event: React.MouseEvent) => event.stopPropagation(),
+ onClick,
+ ),
+ 'onMouseDown': chain(
+ (event: React.MouseEvent) => event.stopPropagation(),
+ onMouseDown,
+ ),
+ ...restProps,
+ } as const,
+ spinButtonContainerProps: {
+ 'className': cn(
+ DFEAULT_NUMBER_INPUT_SPIN_BUTTON_CONTAINER_CLASSNAME,
+ spinButtonContainerClassName,
+ ),
+ 'data-slot': 'spin-button-container',
+ ...restSpinButtonContainerClassName,
+ },
+ incrementButtonProps: {
+ 'action': 'increment',
+ 'data-slot': 'increment-button',
+ 'className': cx(incrementButtonClassName),
+ 'onPress': chain(increment, incrementButtonOnPress),
+ 'onLongPress': chain(incrementWithVariableSpeed, incrementOnLongPress),
+ 'onPointerUp': chain(stopIncrementing, incrementOnPointerUp),
+ 'onPointerLeave': chain(stopIncrementing, incrementOnPointerLeave),
+ 'onPointerCancel': chain(stopIncrementing, incrementOnPointerCancel),
+ ...restIncrementButtonClassName,
+ } as const,
+ decrementButtonProps: {
+ 'action': 'decrement',
+ 'data-slot': 'decrement-button',
+ 'className': cx(decrementButtonClassName),
+ 'onPress': chain(decrement, decrementButtonOnPress),
+ 'onLongPress': chain(decrementWithVariableSpeed, decrementOnLongPress),
+ 'onPointerUp': chain(stopDecrementing, decrementOnPointerUp),
+ 'onPointerLeave': chain(stopDecrementing, decrementOnPointerLeave),
+ 'onPointerCancel': chain(stopDecrementing, decrementOnPointerCancel),
+ ...restDecrementButtonClassName,
+ } as const,
+ separatorProps,
+ utilityProps: {
+ startContent: functionCallOrValue(startContent, value),
+ children: functionCallOrValue(children, value),
+ endContent: functionCallOrValue(endContent, value),
+ },
+ } as const
+}
+
+export { useNumberInput }
diff --git a/src/components/number-input/hooks/use-number-spin.ts b/src/components/number-input/hooks/use-number-spin.ts
new file mode 100644
index 0000000..6952fae
--- /dev/null
+++ b/src/components/number-input/hooks/use-number-spin.ts
@@ -0,0 +1,162 @@
+import { ZERO } from '@renderui/constants'
+import React from 'react'
+
+import { focusInput } from '@/components/_shared/utils/focus-input'
+import { SPIN_TIMEOUT } from '@/components/number-input/constants/constants'
+import { NumberInputProps } from '@/components/number-input/types/number-input'
+import { getNewIntervalDuration } from '@/components/number-input/utils/get-new-interval-duration'
+import { isValidValue } from '@/components/number-input/utils/is-valid-value'
+
+type UseNumberSpinArgs = {
+ value: NumberInputProps['value']
+ min: NumberInputProps['min']
+ max: NumberInputProps['max']
+ step: NumberInputProps['step']
+ pattern: NumberInputProps['pattern']
+ setValue: React.Dispatch>
+ onSpin: NumberInputProps['onSpin']
+ onSpinIncrement: NumberInputProps['onSpinIncrement']
+ onSpinDecrement: NumberInputProps['onSpinDecrement']
+}
+
+function useNumberSpin(args: UseNumberSpinArgs, inputRef: React.RefObject) {
+ const { value, min, max, step, pattern, setValue, onSpin, onSpinIncrement, onSpinDecrement } =
+ args
+
+ // track current value with a ref, used to be able to access the current value
+ // without the setValue callback function, safer access to current value as
+ // setValue can be any externaly passed setter
+ const currentValueRef = React.useRef(value as string)
+
+ // update the current value when value chages
+ React.useEffect(() => {
+ currentValueRef.current = value as string
+ }, [value])
+
+ const clickTimeout = React.useRef(null)
+ const incrementInterval = React.useRef(null)
+ const decrementInterval = React.useRef(null)
+ const pressDuration = React.useRef(ZERO)
+
+ const focusInputOnClickTimeout = () => {
+ if (clickTimeout.current) {
+ clearTimeout(clickTimeout.current)
+ clickTimeout.current = null
+ }
+
+ clickTimeout.current = setTimeout(() => {
+ if (inputRef.current) focusInput(inputRef)
+ }, SPIN_TIMEOUT)
+ }
+
+ const handleSpin = (
+ action: 'increment' | 'decrement',
+ onActionSpinHandler: ((value: string) => void) | undefined,
+ ) => {
+ const previousValue =
+ currentValueRef.current === undefined ? ZERO : Number(currentValueRef.current)
+
+ const getNewValue = () => {
+ if (action === 'decrement') {
+ return (previousValue - Number(step)).toString()
+ }
+
+ return (previousValue + Number(step)).toString()
+ }
+
+ const newValue = getNewValue()
+
+ const isValid = isValidValue({ value: newValue, min, max, pattern })
+
+ if (!isValid) {
+ setValue(String(previousValue) || '')
+
+ return
+ }
+
+ if (onSpin) onSpin(newValue)
+
+ if (onActionSpinHandler) onActionSpinHandler(newValue)
+
+ currentValueRef.current = newValue
+
+ setValue(currentValueRef.current)
+
+ focusInputOnClickTimeout()
+ }
+
+ const increment = () => handleSpin('increment', onSpinIncrement)
+
+ const decrement = () => handleSpin('decrement', onSpinDecrement)
+
+ const incrementWithVariableSpeed = () => {
+ increment()
+
+ if (incrementInterval.current) clearInterval(incrementInterval.current)
+
+ const newIntervalDuration = getNewIntervalDuration(pressDuration.current)
+
+ incrementInterval.current = setInterval(incrementWithVariableSpeed, newIntervalDuration)
+ pressDuration.current += newIntervalDuration // increase press duration
+ }
+
+ const stopIncrementing = () => {
+ if (incrementInterval.current) {
+ clearInterval(incrementInterval.current)
+ incrementInterval.current = null
+ focusInput(inputRef)
+ }
+
+ pressDuration.current = ZERO // reset press duration
+ }
+
+ const decrementWithVariableSpeed = () => {
+ decrement()
+
+ if (decrementInterval.current) clearInterval(decrementInterval.current)
+
+ const newIntervalDuration = getNewIntervalDuration(pressDuration.current)
+
+ decrementInterval.current = setInterval(decrementWithVariableSpeed, newIntervalDuration)
+ pressDuration.current += newIntervalDuration // increase press duration
+ }
+
+ const stopDecrementing = () => {
+ if (decrementInterval.current) {
+ clearInterval(decrementInterval.current)
+ decrementInterval.current = null
+ focusInput(inputRef)
+ }
+
+ pressDuration.current = ZERO // reset press duration
+ }
+
+ const clearIntervals = () => {
+ if (clickTimeout.current) {
+ clearTimeout(clickTimeout.current)
+ clickTimeout.current = null
+ }
+
+ if (incrementInterval.current) {
+ clearInterval(incrementInterval.current)
+ incrementInterval.current = null
+ }
+
+ if (decrementInterval.current) {
+ clearInterval(decrementInterval.current)
+ decrementInterval.current = null
+ }
+ }
+
+ return {
+ increment,
+ decrement,
+ incrementWithVariableSpeed,
+ decrementWithVariableSpeed,
+ stopIncrementing,
+ stopDecrementing,
+ clearIntervals,
+ }
+}
+
+export { useNumberSpin }
diff --git a/src/components/number-input/hooks/use-on-change.ts b/src/components/number-input/hooks/use-on-change.ts
new file mode 100644
index 0000000..de257fc
--- /dev/null
+++ b/src/components/number-input/hooks/use-on-change.ts
@@ -0,0 +1,30 @@
+import { HUNDRED } from '@renderui/constants'
+
+import { NumberInputProps } from '@/components/number-input/types/number-input'
+import { isValidValue } from '@/components/number-input/utils/is-valid-value'
+
+type UseOnChangeArgs = Pick<
+ NumberInputProps,
+ 'precision' | 'value' | 'min' | 'max' | 'pattern' | 'onChange'
+> & {
+ setValue: React.Dispatch>
+}
+
+const getOnChange =
+ ({ min, max, pattern, precision, setValue, onChange }: UseOnChangeArgs) =>
+ (event: React.ChangeEvent) => {
+ const eventValue = event.target.value
+
+ if (!isValidValue({ value: eventValue, min, max, pattern })) return
+
+ const fixedValueString =
+ !precision || precision === 'smart'
+ ? eventValue
+ : (Math.round(Number(eventValue) * HUNDRED) / HUNDRED).toFixed(precision)
+
+ setValue(fixedValueString)
+
+ if (onChange) onChange(event)
+ }
+
+export { getOnChange }
diff --git a/src/components/number-input/index.ts b/src/components/number-input/index.ts
new file mode 100644
index 0000000..f9c6acc
--- /dev/null
+++ b/src/components/number-input/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/number-input/components/number-input'
+export * from '@/components/number-input/types/number-input'
diff --git a/src/components/number-input/styles/styles.module.css b/src/components/number-input/styles/styles.module.css
new file mode 100644
index 0000000..1840c26
--- /dev/null
+++ b/src/components/number-input/styles/styles.module.css
@@ -0,0 +1,11 @@
+.number-input::-webkit-inner-spin-button,
+.number-input::-webkit-outer-spin-button {
+ appearance: none;
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+.number-input {
+ appearance: none;
+ -moz-appearance: textfield;
+}
diff --git a/src/components/number-input/types/number-input.ts b/src/components/number-input/types/number-input.ts
new file mode 100644
index 0000000..a9d635c
--- /dev/null
+++ b/src/components/number-input/types/number-input.ts
@@ -0,0 +1,44 @@
+import { Separator } from '@radix-ui/react-select'
+import { Simplify } from '@renderui/types'
+import React from 'react'
+
+import { inputContainerClasses } from '@/components/_shared/classes/input-container-classes'
+import { AsChildProp } from '@/components/_shared/types/as-child'
+import { NonNullableVariantProps } from '@/components/_shared/types/variants'
+import { Aria } from '@/components/aria'
+import { ButtonProps } from '@/components/button'
+
+type NumberInputRef = React.ElementRef<'input'>
+
+type NumberInputPrimitiveProps = Omit<
+ React.ComponentPropsWithoutRef<'input'>,
+ 'children' | 'disabled' | 'readonly' | 'required'
+>
+
+type NumberInputCustomProps = {
+ isRequired?: boolean
+ isDisabled?: boolean
+ isInvalid?: boolean
+ isReadOnly?: boolean
+ children?: React.ReactNode | ((value: string) => React.ReactNode)
+ startContent?: React.ReactNode | ((value: string) => React.ReactNode)
+ endContent?: React.ReactNode | ((value: string) => React.ReactNode)
+ inputContainerProps?: React.ComponentPropsWithoutRef
+ spinButtonContainerProps?: Simplify & AsChildProp>
+ incrementButtonProps?: ButtonProps
+ decrementButtonProps?: ButtonProps
+ separatorProps?: React.ComponentPropsWithoutRef
+ precision?: number | 'smart'
+ onValueChange?: (value: string) => void
+ onSpin?: (value: string) => void
+ onSpinIncrement?: (value: string) => void
+ onSpinDecrement?: (value: string) => void
+}
+
+type NumberInputVariantProps = NonNullableVariantProps
+
+type NumberInputProps = Simplify<
+ NumberInputPrimitiveProps & NumberInputCustomProps & NumberInputVariantProps & AsChildProp
+>
+
+export type { NumberInputProps, NumberInputRef }
diff --git a/src/components/number-input/types/number-spin-button.ts b/src/components/number-input/types/number-spin-button.ts
new file mode 100644
index 0000000..6807759
--- /dev/null
+++ b/src/components/number-input/types/number-spin-button.ts
@@ -0,0 +1,7 @@
+import { Simplify } from '@renderui/types'
+
+import { ButtonProps } from '@/components/button'
+
+type NumberSpinButtonProps = Simplify
+
+export type { NumberSpinButtonProps }
diff --git a/src/components/number-input/utils/get-handle-key-press-capture.ts b/src/components/number-input/utils/get-handle-key-press-capture.ts
new file mode 100644
index 0000000..081c9f5
--- /dev/null
+++ b/src/components/number-input/utils/get-handle-key-press-capture.ts
@@ -0,0 +1,47 @@
+import { ZERO } from '@renderui/constants'
+import React from 'react'
+
+import { NumberInputProps } from '@/components/number-input/types/number-input'
+import { isValidValue } from '@/components/number-input/utils/is-valid-value'
+
+type GetHandleKeyPressCaptureArgs = {
+ min: NumberInputProps['min']
+ max: NumberInputProps['max']
+ step: NumberInputProps['step']
+ pattern: NumberInputProps['pattern']
+ setValue: React.Dispatch>
+}
+
+function getHandleKeyPressCapture(props: GetHandleKeyPressCaptureArgs) {
+ const { min, max, step, pattern, setValue } = props
+
+ const updateValue = (previousValue: string | undefined, increment: number) => {
+ const numericValue = Number(previousValue)
+ const safeValue = Number.isNaN(numericValue) ? ZERO : numericValue
+
+ const newValue = safeValue + increment
+
+ const isValid = isValidValue({
+ value: newValue.toString(),
+ min,
+ max,
+ pattern,
+ })
+
+ return isValid ? String(newValue) : previousValue
+ }
+
+ return (event: React.KeyboardEvent) => {
+ if (event.key === 'ArrowUp') {
+ event.preventDefault()
+
+ setValue((previousValue) => updateValue(previousValue, Number(step)))
+ } else if (event.key === 'ArrowDown') {
+ event.preventDefault()
+
+ setValue((previousValue) => updateValue(previousValue, -Number(step)))
+ }
+ }
+}
+
+export { getHandleKeyPressCapture }
diff --git a/src/components/number-input/utils/get-new-interval-duration.ts b/src/components/number-input/utils/get-new-interval-duration.ts
new file mode 100644
index 0000000..1c6a896
--- /dev/null
+++ b/src/components/number-input/utils/get-new-interval-duration.ts
@@ -0,0 +1,21 @@
+import {
+ ACCELERATED_SPIN_HOLD_TIMEOUT,
+ ACCELERATED_SPIN_TRESHOLD_TIMEOUT,
+ HOLDING_SPIN_TIMEOUT,
+ INITIAL_SPIN_HOLD_TIMEOUT,
+ INITIAL_SPIN_TRESHOLD_TIMEOUT,
+} from '@/components/number-input/constants/constants'
+
+function getNewIntervalDuration(duration: number) {
+ if (duration === INITIAL_SPIN_TRESHOLD_TIMEOUT) {
+ return INITIAL_SPIN_HOLD_TIMEOUT // initial delay of 500ms
+ }
+
+ if (duration < ACCELERATED_SPIN_TRESHOLD_TIMEOUT) {
+ return ACCELERATED_SPIN_HOLD_TIMEOUT // then repeat once with delay of 100ms
+ }
+
+ return HOLDING_SPIN_TIMEOUT // then repeat every 10ms
+}
+
+export { getNewIntervalDuration }
diff --git a/src/components/number-input/utils/is-valid-value.ts b/src/components/number-input/utils/is-valid-value.ts
new file mode 100644
index 0000000..84f733a
--- /dev/null
+++ b/src/components/number-input/utils/is-valid-value.ts
@@ -0,0 +1,38 @@
+import { NUMERIC_REGEX } from '@/components/number-input/constants/constants'
+import { NumberInputProps } from '@/components/number-input/types/number-input'
+
+type IsValidValueArgs = {
+ value: string | undefined
+ min: NumberInputProps['min']
+ max: NumberInputProps['max']
+ pattern: NumberInputProps['pattern']
+}
+
+const isValidValue = (args: IsValidValueArgs) => {
+ const { value, min, max, pattern } = args
+
+ if (value === undefined) return false
+
+ if (!NUMERIC_REGEX.test(value)) return false
+
+ // Convert the new value to a number
+ const numericValue = Number(value)
+
+ // Convert min and max to numbers for comparison
+ const numericMin = min === undefined ? undefined : Number(min)
+ const numericMax = max === undefined ? undefined : Number(max)
+
+ // Check if the new value is within the min and max range
+ if (
+ (numericMin !== undefined && numericValue < numericMin) ||
+ (numericMax !== undefined && numericValue > numericMax)
+ ) {
+ return false
+ }
+
+ // Check if the new value matches the pattern
+ // eslint-disable-next-line security/detect-non-literal-regexp
+ return !(pattern !== undefined && !new RegExp(pattern).test(value))
+}
+
+export { isValidValue }
diff --git a/src/components/overlay/components/overlay.tsx b/src/components/overlay/components/overlay.tsx
new file mode 100644
index 0000000..b27388d
--- /dev/null
+++ b/src/components/overlay/components/overlay.tsx
@@ -0,0 +1,20 @@
+'use client'
+
+import { DialogOverlay } from '@radix-ui/react-dialog'
+import { cn } from '@renderui/utils'
+import React from 'react'
+
+import { DEFAULT_OVERLAY_CLASSNAME } from '@/components/overlay/constants/constants'
+import { OverlayProps, OverlayRef } from '@/components/overlay/types/overlay'
+
+const Overlay = React.forwardRef((props, ref) => {
+ const { className, ...restProps } = props
+
+ return (
+
+ )
+})
+
+Overlay.displayName = 'Overlay'
+
+export { Overlay }
diff --git a/src/components/overlay/constants/constants.ts b/src/components/overlay/constants/constants.ts
new file mode 100644
index 0000000..3348282
--- /dev/null
+++ b/src/components/overlay/constants/constants.ts
@@ -0,0 +1,4 @@
+const DEFAULT_OVERLAY_CLASSNAME =
+ 'render-ui-overlay duration-medium data-[state=open]:animate-overlay-fade-in data-[state=closed]:animate-overlay-fade-out fixed inset-0 z-50 bg-black/70'
+
+export { DEFAULT_OVERLAY_CLASSNAME }
diff --git a/src/components/overlay/index.ts b/src/components/overlay/index.ts
new file mode 100644
index 0000000..d30c7e0
--- /dev/null
+++ b/src/components/overlay/index.ts
@@ -0,0 +1,2 @@
+export * from '@/components/overlay/components/overlay'
+export * from '@/components/overlay/types/overlay'
diff --git a/src/components/overlay/types/overlay.ts b/src/components/overlay/types/overlay.ts
new file mode 100644
index 0000000..1cd5247
--- /dev/null
+++ b/src/components/overlay/types/overlay.ts
@@ -0,0 +1,9 @@
+import { DialogOverlay as OverlayPrimitive } from '@radix-ui/react-dialog'
+
+type OverlayPrimitiveType = typeof OverlayPrimitive
+
+type OverlayRef = React.ElementRef
+
+type OverlayProps = React.ComponentPropsWithRef
+
+export type { OverlayProps, OverlayRef }
diff --git a/src/components/popover/components/popover-content.tsx b/src/components/popover/components/popover-content.tsx
new file mode 100644
index 0000000..44686d7
--- /dev/null
+++ b/src/components/popover/components/popover-content.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import {
+ PopoverArrow as PopoverArrowPrimitive,
+ PopoverContent as PopoverContentPrimitive,
+ PopoverPortal as PopoverPortalPrimitive,
+} from '@radix-ui/react-popover'
+import { cn, getOptionalObject } from '@renderui/utils'
+import React from 'react'
+
+import {
+ DEFAULT_POPOVER_ARROW_CLASSNAME,
+ DEFAULT_POPOVER_CONTENT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_HEIGHT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MAX_HEIGHT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MAX_WIDTH_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MIN_HEIGHT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MIN_WIDTH_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_WIDTH_CLASSNAME,
+} from '@/components/popover/constants/constants'
+import { PopoverContentProps, PopoverContentRef } from '@/components/popover/types/popover-content'
+
+const PopoverContent = React.forwardRef((props, ref) => {
+ const {
+ className,
+ children,
+ portalContainer,
+ forceMount,
+ hasTriggerHeight,
+ hasTriggerWidth,
+ hasTriggerMinWidth,
+ hasTriggerMinHeight,
+ hasTriggerMaxWidth,
+ hasTriggerMaxHeight,
+ arrowProps,
+ hasArrow = true,
+ align = 'center',
+ sideOffset = 4,
+ ...restProps
+ } = props
+
+ const { className: arrowClassName, ...restArrowProps } = getOptionalObject(arrowProps)
+
+ return (
+
+
+ {children}
+ {hasArrow ? (
+
+ ) : null}
+
+
+ )
+})
+
+PopoverContent.displayName = 'PopoverContent'
+
+export { PopoverContent }
diff --git a/src/components/popover/components/popover-trigger.tsx b/src/components/popover/components/popover-trigger.tsx
new file mode 100644
index 0000000..545baf3
--- /dev/null
+++ b/src/components/popover/components/popover-trigger.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import { PopoverTrigger as PopoverTriggerPrimitive } from '@radix-ui/react-popover'
+import { cx } from '@renderui/utils'
+import React from 'react'
+
+import { Button } from '@/components/button'
+import { DEFAULT_POPOVER_TRIGGER_CLASSNAME } from '@/components/popover/constants/constants'
+import { PopoverTriggerProps, PopoverTriggerRef } from '@/components/popover/types/popover-trigger'
+
+const PopoverTrigger = React.forwardRef((props, ref) => {
+ const {
+ className,
+ variant = 'solid',
+ color = 'mode-accent',
+ hasRipple = true,
+ ...restProps
+ } = props
+
+ return (
+
+
+
+ )
+})
+
+PopoverTrigger.displayName = 'PopoverTrigger'
+
+export { PopoverTrigger }
diff --git a/src/components/popover/components/popover.tsx b/src/components/popover/components/popover.tsx
new file mode 100644
index 0000000..519449f
--- /dev/null
+++ b/src/components/popover/components/popover.tsx
@@ -0,0 +1,7 @@
+'use client'
+
+import { Popover as PopoverPrimitive } from '@radix-ui/react-popover'
+
+const Popover = PopoverPrimitive
+
+export { Popover }
diff --git a/src/components/popover/constants/constants.ts b/src/components/popover/constants/constants.ts
new file mode 100644
index 0000000..defca88
--- /dev/null
+++ b/src/components/popover/constants/constants.ts
@@ -0,0 +1,31 @@
+const DEFAULT_POPOVER_CONTENT_CLASSNAME =
+ 'render-ui-popover-content data-[state=closed]:data-[side=bottom]:animate-popover-exit-from-top-and-fade-out data-[state=closed]:data-[side=top]:animate-popover-exit-from-bottom-and-fade-out data-[state=closed]:data-[side=right]:animate-popover-exit-from-left-and-fade-out data-[state=closed]:data-[side=left]:animate-popover-exit-from-right-and-fade-out data-[state=open]:data-[side=bottom]:animate-popover-enter-to-top-and-fade-in data-[state=open]:data-[side=left]:animate-popover-enter-to-right-and-fade-in data-[state=open]:data-[side=right]:animate-popover-enter-to-left-and-fade-in data-[state=open]:data-[side=top]:animate-popover-enter-to-bottom-and-fade-in z-50 box-border w-fit rounded-md border bg-background border-mode-accent text-mode-contrast p-4 shadow-md outline-none will-change-[transform,opacity] data-[side=bottom]:origin-top data-[side=left]:origin-right data-[side=right]:origin-left data-[side=top]:origin-bottom'
+
+const DEFAULT_POPOVER_TRIGGER_CLASSNAME = 'render-ui-popover-trigger'
+
+const POPOVER_CONTENT_TRIGGER_WIDTH_CLASSNAME = 'w-[var(--radix-popover-trigger-width)]'
+
+const POPOVER_CONTENT_TRIGGER_HEIGHT_CLASSNAME = 'h-[var(--radix-popover-trigger-width)]'
+
+const POPOVER_CONTENT_TRIGGER_MIN_WIDTH_CLASSNAME = 'min-w-[var(--radix-popover-trigger-width)]'
+
+const POPOVER_CONTENT_TRIGGER_MIN_HEIGHT_CLASSNAME = 'min-h-[var(--radix-popover-trigger-width)]'
+
+const POPOVER_CONTENT_TRIGGER_MAX_WIDTH_CLASSNAME = 'max-w-[var(--radix-popover-trigger-width)]'
+
+const POPOVER_CONTENT_TRIGGER_MAX_HEIGHT_CLASSNAME = 'max-h-[var(--radix-popover-trigger-width)]'
+
+const DEFAULT_POPOVER_ARROW_CLASSNAME =
+ 'render-ui-popover-arrow drop-shadow-[0_1px_0_rgba(var(--mode-accent))]'
+
+export {
+ DEFAULT_POPOVER_ARROW_CLASSNAME,
+ DEFAULT_POPOVER_CONTENT_CLASSNAME,
+ DEFAULT_POPOVER_TRIGGER_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_HEIGHT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MAX_HEIGHT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MAX_WIDTH_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MIN_HEIGHT_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_MIN_WIDTH_CLASSNAME,
+ POPOVER_CONTENT_TRIGGER_WIDTH_CLASSNAME,
+}
diff --git a/src/components/popover/index.ts b/src/components/popover/index.ts
new file mode 100644
index 0000000..3e046e7
--- /dev/null
+++ b/src/components/popover/index.ts
@@ -0,0 +1,6 @@
+export * from '@/components/popover/components/popover'
+export * from '@/components/popover/components/popover-content'
+export * from '@/components/popover/components/popover-trigger'
+export * from '@/components/popover/types/popover'
+export * from '@/components/popover/types/popover-content'
+export * from '@/components/popover/types/popover-trigger'
diff --git a/src/components/popover/types/popover-content.ts b/src/components/popover/types/popover-content.ts
new file mode 100644
index 0000000..6d51022
--- /dev/null
+++ b/src/components/popover/types/popover-content.ts
@@ -0,0 +1,23 @@
+import {
+ PopoverArrow as PopoverArrowPrimitive,
+ PopoverContent as PopoverContentPrimitive,
+} from '@radix-ui/react-popover'
+import React from 'react'
+
+type PopoverContentPrimitiveType = typeof PopoverContentPrimitive
+
+type PopoverContentRef = React.ElementRef
+
+type PopoverContentProps = React.ComponentPropsWithoutRef & {
+ portalContainer?: HTMLElement | null | undefined
+ hasTriggerHeight?: boolean
+ hasTriggerWidth?: boolean
+ hasTriggerMinWidth?: boolean
+ hasTriggerMinHeight?: boolean
+ hasTriggerMaxWidth?: boolean
+ hasTriggerMaxHeight?: boolean
+ hasArrow?: boolean
+ arrowProps?: React.ComponentPropsWithoutRef