├── .prettierignore ├── content └── docs │ ├── meta.json │ └── index.mdx ├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── (home) │ ├── page.tsx │ ├── layout.tsx │ └── _ │ │ ├── demo.tsx │ │ └── controls.tsx ├── source.ts ├── api │ └── search │ │ └── route.ts ├── layout.config.ts ├── docs │ ├── layout.tsx │ └── [[...slug]] │ │ └── page.tsx ├── layout.tsx └── globals.css ├── .prettierrc ├── source.config.ts ├── postcss.config.mjs ├── lib ├── utils.ts └── hooks.ts ├── .eslintrc.json ├── next.config.mjs ├── components.json ├── README.md ├── .gitignore ├── tsconfig.json ├── components ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── checkbox.tsx │ ├── switch.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── toggle.tsx │ ├── button.tsx │ ├── accordion.tsx │ ├── drawer.tsx │ └── form.tsx ├── labeled-checkbox.tsx ├── labeled-input.tsx ├── examples │ ├── single-file.tsx │ ├── multi-images.tsx │ └── multi-file.tsx ├── labeled-switch.tsx └── dropzone.tsx ├── LICENSE.md ├── package.json ├── scripts └── build.ts ├── tailwind.config.ts └── public └── dropzone.json /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": ["Hello"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janglad/shadcn-dropzone/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janglad/shadcn-dropzone/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janglad/shadcn-dropzone/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Demo } from "./_/demo"; 2 | 3 | export default function HomePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /source.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, defineDocs } from "fumadocs-mdx/config"; 2 | 3 | export const { docs, meta } = defineDocs(); 4 | 5 | export default defineConfig(); 6 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/source.ts: -------------------------------------------------------------------------------- 1 | import { docs, meta } from "@/.source"; 2 | import { loader } from "fumadocs-core/source"; 3 | import { createMDXSource } from "fumadocs-mdx"; 4 | 5 | export const source = loader({ 6 | baseUrl: "/docs", 7 | source: createMDXSource(docs, meta), 8 | }); 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const roundUpTo = (num: number, decimals: number) => { 9 | return Math.ceil(num * 10 ** decimals) / 10 ** decimals; 10 | }; 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "next/typescript", 5 | "plugin:tailwindcss/recommended", 6 | "plugin:jsx-a11y/strict" 7 | ], 8 | "plugins": ["react-compiler"], 9 | "rules": { 10 | "react-compiler/react-compiler": "error" 11 | }, 12 | "ignorePatterns": ["**/components/ui/**"] 13 | } 14 | -------------------------------------------------------------------------------- /app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { HomeLayout } from "fumadocs-ui/layouts/home" 3 | import { baseOptions } from "../layout.config"; 4 | 5 | export default function Layout({ 6 | children, 7 | }: { 8 | children: ReactNode; 9 | }): React.ReactElement { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | redirects: async () => [ 8 | { 9 | source: "/", 10 | destination: "/docs", 11 | permanent: false, 12 | }, 13 | ], 14 | }; 15 | 16 | export default withMDX(nextConfig); 17 | -------------------------------------------------------------------------------- /app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from '@/app/source'; 2 | import { createSearchAPI } from 'fumadocs-core/search/server'; 3 | 4 | export const { GET } = createSearchAPI('advanced', { 5 | indexes: source.getPages().map((page) => ({ 6 | title: page.data.title, 7 | structuredData: page.data.structuredData, 8 | id: page.url, 9 | url: page.url, 10 | })), 11 | }); 12 | -------------------------------------------------------------------------------- /app/layout.config.ts: -------------------------------------------------------------------------------- 1 | import { type BaseLayoutProps } from "fumadocs-ui/layouts/shared"; 2 | 3 | /** 4 | * Shared layout configuration 5 | * 6 | * you cna configure layouts individually from: 7 | * Home Layout: 8 | */ 9 | export const baseOptions: BaseLayoutProps = { 10 | nav: { 11 | title: "Dropzone", 12 | }, 13 | githubUrl: "https://github.com/janglad/shadcn-dropzone", 14 | }; 15 | -------------------------------------------------------------------------------- /app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/app/source"; 2 | import { DocsLayout } from "fumadocs-ui/layouts/docs"; 3 | import type { ReactNode } from "react"; 4 | import { baseOptions } from "../layout.config"; 5 | 6 | export default function Layout({ children }: { children: ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useIsMobile = () => { 4 | const [isMobile, setIsMobile] = useState(false); 5 | 6 | const handleResize = () => { 7 | setIsMobile(window.innerWidth <= 768); 8 | }; 9 | 10 | useEffect(() => { 11 | handleResize(); 12 | window.addEventListener("resize", handleResize); 13 | 14 | return () => { 15 | window.removeEventListener("resize", handleResize); 16 | }; 17 | }, []); 18 | 19 | return isMobile; 20 | }; 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShadCn style Dropzone 2 | 3 | A dropzone component built in the style of ShadCn. Uses the `useDropzone` hook from react-dropzone, builds on ShadCn primitives and aims to be fully accessible. 4 | 5 | ## Todo 6 | 7 | - [ ] Integrate with React Hook Form 8 | 9 | ## Installation 10 | 11 | Make sure you have ShadCn set up in your project and run `pnpx shadcn@latest add 'https://shadcn-dropzone.vercel.app/dropzone.json'`. 12 | 13 | Or copy [this file](/components/dropzone.tsx) and update imports as needed. 14 | 15 | ## Usage 16 | 17 | Check out the[docs](https://shadcn-dropzone.vercel.app/). 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | #From Fumadocs 39 | # generated content 40 | .contentlayer 41 | .content-collections 42 | .source 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { RootProvider } from "fumadocs-ui/provider"; 2 | import type { Metadata } from "next"; 3 | import localFont from "next/font/local"; 4 | import "./globals.css"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Shadcn/ui Dropzone", 19 | description: 20 | "Dropzone component created in the style of shadcn/ui using React Dropzone.", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | {children} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 janglad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 | import { Check } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/app/source"; 2 | import defaultMdxComponents from "fumadocs-ui/mdx"; 3 | import { 4 | DocsBody, 5 | DocsDescription, 6 | DocsPage, 7 | DocsTitle, 8 | } from "fumadocs-ui/page"; 9 | import type { Metadata } from "next"; 10 | import { notFound } from "next/navigation"; 11 | 12 | export default async function Page({ 13 | params, 14 | }: { 15 | params: { slug?: string[] }; 16 | }) { 17 | const page = source.getPage(params.slug); 18 | if (!page) notFound(); 19 | 20 | const MDX = page.data.body; 21 | 22 | return ( 23 | 24 | {page.data.title} 25 | {page.data.description} 26 | 27 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export async function generateStaticParams() { 38 | return source.generateParams(); 39 | } 40 | 41 | export function generateMetadata({ params }: { params: { slug?: string[] } }) { 42 | const page = source.getPage(params.slug); 43 | if (!page) notFound(); 44 | 45 | return { 46 | title: page.data.title, 47 | description: page.data.description, 48 | } satisfies Metadata; 49 | } 50 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverContent, PopoverTrigger }; 32 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TogglePrimitive from "@radix-ui/react-toggle"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", 17 | }, 18 | size: { 19 | default: "h-10 px-3", 20 | sm: "h-9 px-2.5", 21 | lg: "h-11 px-5", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | }, 29 | ); 30 | 31 | const Toggle = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef & 34 | VariantProps 35 | >(({ className, variant, size, ...props }, ref) => ( 36 | 41 | )); 42 | 43 | Toggle.displayName = TogglePrimitive.Root.displayName; 44 | 45 | export { Toggle, toggleVariants }; 46 | -------------------------------------------------------------------------------- /components/labeled-checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@/components/ui/checkbox"; 2 | import { 3 | FormControl, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | FormMessage, 8 | } from "@/components/ui/form"; 9 | import type { 10 | Control, 11 | ControllerRenderProps, 12 | FieldValues, 13 | UseControllerProps, 14 | } from "react-hook-form"; 15 | 16 | interface LabeledCheckboxProps 17 | extends UseControllerProps { 18 | // Optional in default type 19 | control: Control; 20 | label?: string; 21 | inputProps?: Omit< 22 | React.ComponentProps, 23 | keyof ControllerRenderProps 24 | >; 25 | } 26 | 27 | export function LabeledCheckbox( 28 | props: LabeledCheckboxProps, 29 | ) { 30 | const { inputProps, ...restProps } = props; 31 | const isInvalid = props.control.getFieldState(props.name).invalid; 32 | 33 | return ( 34 | ( 37 | 38 |
39 | {props.label === undefined ? null : ( 40 | {props.label} 41 | )} 42 | 43 |
44 | 45 | 46 | 53 | 54 |
55 | )} 56 | /> 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /components/labeled-input.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormDescription, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | FormMessage, 8 | } from "@/components/ui/form"; 9 | import { Input } from "@/components/ui/input"; 10 | import { cn } from "@/lib/utils"; 11 | import type { 12 | Control, 13 | ControllerRenderProps, 14 | FieldValues, 15 | UseControllerProps, 16 | } from "react-hook-form"; 17 | 18 | interface LabeledInputProps 19 | extends UseControllerProps { 20 | className?: string; 21 | // Optional in default type 22 | control: Control; 23 | description?: string; 24 | label?: string; 25 | inputProps?: Omit< 26 | React.ComponentProps, 27 | keyof ControllerRenderProps 28 | >; 29 | } 30 | 31 | export function LabeledInput( 32 | props: LabeledInputProps, 33 | ) { 34 | const { inputProps, ...restProps } = props; 35 | const isInvalid = props.control.getFieldState(props.name).invalid; 36 | 37 | return ( 38 | ( 41 | 44 |
45 | {props.label === undefined ? null : ( 46 | {props.label} 47 | )} 48 | 49 |
50 | 51 | 52 | 53 | 54 | {props.description === undefined ? null : ( 55 | {props.description} 56 | )} 57 |
58 | )} 59 | /> 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadcn-dropzone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.9.0", 13 | "@radix-ui/react-accordion": "^1.2.1", 14 | "@radix-ui/react-avatar": "^1.1.1", 15 | "@radix-ui/react-checkbox": "^1.1.2", 16 | "@radix-ui/react-dialog": "^1.1.2", 17 | "@radix-ui/react-label": "^2.1.0", 18 | "@radix-ui/react-popover": "^1.1.2", 19 | "@radix-ui/react-slot": "^1.1.0", 20 | "@radix-ui/react-switch": "^1.1.1", 21 | "@radix-ui/react-toggle": "^1.1.0", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.1.1", 24 | "fumadocs-core": "^14.2.0", 25 | "fumadocs-mdx": "^11.1.1", 26 | "fumadocs-ui": "^14.2.0", 27 | "lucide-react": "^0.452.0", 28 | "neverthrow": "^8.0.0", 29 | "next": "14.2.15", 30 | "react": "^18", 31 | "react-dom": "^18", 32 | "react-dropzone": "^14.2.9", 33 | "react-hook-form": "^7.53.0", 34 | "tailwind-merge": "^2.5.3", 35 | "tailwindcss-animate": "^1.0.7", 36 | "vaul": "^1.1.0", 37 | "zod": "^3.23.8" 38 | }, 39 | "devDependencies": { 40 | "@types/mdx": "^2.0.13", 41 | "@types/node": "^20", 42 | "@types/react": "^18", 43 | "@types/react-dom": "^18", 44 | "eslint": "^8", 45 | "eslint-config-next": "14.2.15", 46 | "eslint-plugin-jsx-a11y": "^6.10.0", 47 | "eslint-plugin-react-compiler": "0.0.0-experimental-07a2ff2-20241017", 48 | "eslint-plugin-tailwindcss": "^3.17.5", 49 | "postcss": "^8", 50 | "prettier": "^3.3.3", 51 | "prettier-plugin-organize-imports": "^4.1.0", 52 | "prettier-plugin-tailwindcss": "^0.6.8", 53 | "tailwindcss": "^3.4.1", 54 | "typescript": "^5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /components/examples/single-file.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dropzone, 4 | DropZoneArea, 5 | DropzoneMessage, 6 | DropzoneTrigger, 7 | useDropzone, 8 | } from "@/components/dropzone"; 9 | import { cn } from "@/lib/utils"; 10 | import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; 11 | 12 | export function SingleFile() { 13 | const dropzone = useDropzone({ 14 | onDropFile: async (file: File) => { 15 | await new Promise((resolve) => setTimeout(resolve, 1000)); 16 | return { 17 | status: "success", 18 | result: URL.createObjectURL(file), 19 | }; 20 | }, 21 | validation: { 22 | accept: { 23 | "image/*": [".png", ".jpg", ".jpeg"], 24 | }, 25 | maxSize: 10 * 1024 * 1024, 26 | maxFiles: 1, 27 | }, 28 | shiftOnMaxFiles: true, 29 | }); 30 | 31 | const avatarSrc = dropzone.fileStatuses[0]?.result; 32 | const isPending = dropzone.fileStatuses[0]?.status === "pending"; 33 | 34 | return ( 35 |
36 | 37 |
38 | 39 |
40 | 41 | 42 | 43 | 44 | JG 45 | 46 |
47 |

Upload a new avatar

48 |

49 | Please select an image smaller than 10MB 50 |

51 |
52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 0 0% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 0 0% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 0 0% 3.9%; 23 | --primary: 0 0% 9%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | --muted: 0 0% 96.1%; 28 | --muted-foreground: 0 0% 45.1%; 29 | --accent: 0 0% 96.1%; 30 | --accent-foreground: 0 0% 9%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --ring: 0 0% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | } 43 | .dark { 44 | --background: 0 0% 3.9%; 45 | --foreground: 0 0% 98%; 46 | --card: 0 0% 3.9%; 47 | --card-foreground: 0 0% 98%; 48 | --popover: 0 0% 3.9%; 49 | --popover-foreground: 0 0% 98%; 50 | --primary: 0 0% 98%; 51 | --primary-foreground: 0 0% 9%; 52 | --secondary: 0 0% 14.9%; 53 | --secondary-foreground: 0 0% 98%; 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | --accent: 0 0% 14.9%; 57 | --accent-foreground: 0 0% 98%; 58 | --destructive: 0 62.8% 30.6%; 59 | --destructive-foreground: 0 0% 98%; 60 | --border: 0 0% 14.9%; 61 | --input: 0 0% 14.9%; 62 | --ring: 0 0% 83.1%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /components/labeled-switch.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormDescription, 4 | FormField, 5 | FormItem, 6 | FormLabel, 7 | FormMessage, 8 | } from "@/components/ui/form"; 9 | import { Switch } from "@/components/ui/switch"; 10 | import { cn } from "@/lib/utils"; 11 | import type { 12 | Control, 13 | ControllerRenderProps, 14 | FieldValues, 15 | UseControllerProps, 16 | } from "react-hook-form"; 17 | 18 | interface LabeledSwitchProps 19 | extends UseControllerProps { 20 | // Optional in default type 21 | control: Control; 22 | className?: string; 23 | label?: string; 24 | description?: string; 25 | inputProps?: Omit< 26 | React.ComponentProps, 27 | keyof ControllerRenderProps 28 | >; 29 | } 30 | 31 | export function LabeledSwitch( 32 | props: LabeledSwitchProps, 33 | ) { 34 | const { inputProps, ...restProps } = props; 35 | const isInvalid = props.control.getFieldState(props.name).invalid; 36 | 37 | return ( 38 | ( 41 | 47 |
48 | {props.label === undefined ? null : ( 49 | {props.label} 50 | )} 51 | {props.description === undefined ? null : ( 52 | {props.description} 53 | )} 54 | 55 |
56 | 57 | 58 | 65 | 66 |
67 | )} 68 | /> 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 4 | import { ChevronDown } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = "AccordionItem"; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )); 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 57 | 58 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; 59 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | // Adopted from https://niels.foo/post/publishing-custom-shadcn-ui-components 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | 5 | export interface Schema { 6 | name: string; 7 | type: "registry:ui"; 8 | registryDependencies: string[]; 9 | dependencies: string[]; 10 | devDependencies: string[]; 11 | tailwind: { 12 | config?: Record; 13 | }; 14 | cssVars: { 15 | light: Record; 16 | dark: Record; 17 | }; 18 | files: Array<{ 19 | path: string; 20 | content: string; 21 | type: "registry:ui"; 22 | }>; 23 | } 24 | 25 | type ComponentDefinition = Partial< 26 | Pick< 27 | Schema, 28 | | "dependencies" 29 | | "devDependencies" 30 | | "registryDependencies" 31 | | "cssVars" 32 | | "tailwind" 33 | > 34 | > & { 35 | name: string; 36 | path: string; 37 | }; 38 | 39 | // Define the components and their dependencies that should be registered 40 | const components: ComponentDefinition[] = [ 41 | { 42 | name: "dropzone", 43 | path: path.join(__dirname, "../components/dropzone.tsx"), 44 | registryDependencies: ["button"], 45 | dependencies: ["react-dropzone"], 46 | }, 47 | ]; 48 | 49 | // Create the registry directory if it doesn't exist 50 | const registry = path.join(__dirname, "../public"); 51 | if (!fs.existsSync(registry)) { 52 | fs.mkdirSync(registry); 53 | } 54 | 55 | // Create the registry files 56 | for (const component of components) { 57 | const content = fs.readFileSync(component.path, "utf8"); 58 | 59 | const schema = { 60 | name: component.name, 61 | type: "registry:ui", 62 | registryDependencies: component.registryDependencies || [], 63 | dependencies: component.dependencies || [], 64 | devDependencies: component.devDependencies || [], 65 | tailwind: component.tailwind || {}, 66 | cssVars: component.cssVars || { 67 | light: {}, 68 | dark: {}, 69 | }, 70 | files: [ 71 | { 72 | path: `${component.name}.tsx`, 73 | content, 74 | type: "registry:ui", 75 | }, 76 | ], 77 | } satisfies Schema; 78 | 79 | fs.writeFileSync( 80 | path.join(registry, `${component.name}.json`), 81 | JSON.stringify(schema, null, 2), 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { createPreset } from "fumadocs-ui/tailwind-plugin"; 2 | import type { Config } from "tailwindcss"; 3 | 4 | const config: Config = { 5 | darkMode: ["class"], 6 | content: [ 7 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 10 | "./mdx-components.{ts,tsx}", 11 | "./node_modules/fumadocs-ui/dist/**/*.js", 12 | ], 13 | theme: { 14 | extend: { 15 | container: { 16 | center: true, 17 | padding: "1rem", 18 | }, 19 | colors: { 20 | background: "hsl(var(--background))", 21 | foreground: "hsl(var(--foreground))", 22 | card: { 23 | DEFAULT: "hsl(var(--card))", 24 | foreground: "hsl(var(--card-foreground))", 25 | }, 26 | popover: { 27 | DEFAULT: "hsl(var(--popover))", 28 | foreground: "hsl(var(--popover-foreground))", 29 | }, 30 | primary: { 31 | DEFAULT: "hsl(var(--primary))", 32 | foreground: "hsl(var(--primary-foreground))", 33 | }, 34 | secondary: { 35 | DEFAULT: "hsl(var(--secondary))", 36 | foreground: "hsl(var(--secondary-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | destructive: { 47 | DEFAULT: "hsl(var(--destructive))", 48 | foreground: "hsl(var(--destructive-foreground))", 49 | }, 50 | border: "hsl(var(--border))", 51 | input: "hsl(var(--input))", 52 | ring: "hsl(var(--ring))", 53 | chart: { 54 | "1": "hsl(var(--chart-1))", 55 | "2": "hsl(var(--chart-2))", 56 | "3": "hsl(var(--chart-3))", 57 | "4": "hsl(var(--chart-4))", 58 | "5": "hsl(var(--chart-5))", 59 | }, 60 | }, 61 | borderRadius: { 62 | lg: "var(--radius)", 63 | md: "calc(var(--radius) - 2px)", 64 | sm: "calc(var(--radius) - 4px)", 65 | }, 66 | keyframes: { 67 | "accordion-down": { 68 | from: { 69 | height: "0", 70 | }, 71 | to: { 72 | height: "var(--radix-accordion-content-height)", 73 | }, 74 | }, 75 | "accordion-up": { 76 | from: { 77 | height: "var(--radix-accordion-content-height)", 78 | }, 79 | to: { 80 | height: "0", 81 | }, 82 | }, 83 | "infinite-progress": { 84 | "0%": { 85 | transform: "translateX(-100%)", 86 | }, 87 | "100%": { 88 | transform: "translateX(100%)", 89 | }, 90 | }, 91 | }, 92 | animation: { 93 | "accordion-down": "accordion-down 0.2s ease-out", 94 | "accordion-up": "accordion-up 0.2s ease-out", 95 | "infinite-progress": 96 | "infinite-progress 3s cubic-bezier(0.37, 0, 0.63, 1) infinite", 97 | }, 98 | }, 99 | }, 100 | plugins: [require("tailwindcss-animate")], 101 | presets: [createPreset()], 102 | }; 103 | export default config; 104 | -------------------------------------------------------------------------------- /components/examples/multi-images.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dropzone, 4 | DropZoneArea, 5 | DropzoneDescription, 6 | DropzoneFileList, 7 | DropzoneFileListItem, 8 | DropzoneMessage, 9 | DropzoneRemoveFile, 10 | DropzoneTrigger, 11 | useDropzone, 12 | } from "@/components/dropzone"; 13 | import { CloudUploadIcon, Trash2Icon } from "lucide-react"; 14 | 15 | export function MultiImages() { 16 | const dropzone = useDropzone({ 17 | onDropFile: async (file: File) => { 18 | await new Promise((resolve) => setTimeout(resolve, 1000)); 19 | return { 20 | status: "success", 21 | result: URL.createObjectURL(file), 22 | }; 23 | }, 24 | validation: { 25 | accept: { 26 | "image/*": [".png", ".jpg", ".jpeg"], 27 | }, 28 | maxSize: 10 * 1024 * 1024, 29 | maxFiles: 10, 30 | }, 31 | }); 32 | 33 | return ( 34 |
35 | 36 |
37 |
38 | 39 | Please select up to 10 images 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 |

Upload images

48 |

49 | Click here or drag and drop to upload 50 |

51 |
52 |
53 |
54 |
55 | 56 | 57 | {dropzone.fileStatuses.map((file) => ( 58 | 63 | {file.status === "pending" && ( 64 |
65 | )} 66 | {file.status === "success" && ( 67 | // eslint-disable-next-line @next/next/no-img-element 68 | {`uploaded-${file.fileName}`} 73 | )} 74 |
75 |
76 |

{file.fileName}

77 |

78 | {(file.file.size / (1024 * 1024)).toFixed(2)} MB 79 |

80 |
81 | 85 | 86 | 87 |
88 | 89 | ))} 90 | 91 | 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ); 17 | Drawer.displayName = "Drawer"; 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger; 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal; 22 | 23 | const DrawerClose = DrawerPrimitive.Close; 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )); 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )); 56 | DrawerContent.displayName = "DrawerContent"; 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ); 67 | DrawerHeader.displayName = "DrawerHeader"; 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ); 78 | DrawerFooter.displayName = "DrawerFooter"; 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )); 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )); 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 106 | 107 | export { 108 | Drawer, 109 | DrawerClose, 110 | DrawerContent, 111 | DrawerDescription, 112 | DrawerFooter, 113 | DrawerHeader, 114 | DrawerOverlay, 115 | DrawerPortal, 116 | DrawerTitle, 117 | DrawerTrigger, 118 | }; 119 | -------------------------------------------------------------------------------- /components/examples/multi-file.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dropzone, 4 | DropZoneArea, 5 | DropzoneDescription, 6 | DropzoneFileList, 7 | DropzoneFileListItem, 8 | DropzoneFileMessage, 9 | DropzoneMessage, 10 | DropzoneRemoveFile, 11 | DropzoneRetryFile, 12 | DropzoneTrigger, 13 | InfiniteProgress, 14 | useDropzone, 15 | } from "@/components/dropzone"; 16 | import { 17 | CloudUploadIcon, 18 | FileIcon, 19 | RotateCcwIcon, 20 | Trash2Icon, 21 | } from "lucide-react"; 22 | 23 | export function MultiFiles() { 24 | const dropzone = useDropzone({ 25 | onDropFile: async () => { 26 | await new Promise((resolve) => 27 | setTimeout(resolve, Math.random() * 500 + 1000), 28 | ); 29 | 30 | if (Math.random() > 0.8) { 31 | return { 32 | status: "error", 33 | error: "Failed to upload file", 34 | }; 35 | } 36 | return { 37 | status: "success", 38 | result: undefined, 39 | }; 40 | }, 41 | validation: { 42 | maxFiles: 10, 43 | }, 44 | }); 45 | 46 | return ( 47 |
48 | 49 |
50 |
51 | 52 | Please select up to 10 files 53 | 54 | 55 |
56 | 57 | 58 | 59 |
60 |

Upload files

61 |

62 | Click here or drag and drop to upload 63 |

64 |
65 |
66 |
67 |
68 | 69 | 70 | {dropzone.fileStatuses.map((file) => ( 71 | 76 |
77 |
78 | 79 |

{file.fileName}

80 |
81 |
82 | {file.status === "error" && ( 83 | 89 | 90 | 91 | )} 92 | 93 | 99 | 100 | 101 |
102 |
103 | 104 |
105 |

{(file.file.size / (1024 * 1024)).toFixed(2)} MB

106 | 107 |
108 |
109 | ))} 110 |
111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { Slot } from "@radix-ui/react-slot"; 5 | import * as React from "react"; 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form"; 14 | 15 | import { Label } from "@/components/ui/label"; 16 | import { cn } from "@/lib/utils"; 17 | 18 | const Form = FormProvider; 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath, 23 | > = { 24 | name: TName; 25 | }; 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue, 29 | ); 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath, 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext); 46 | const itemContext = React.useContext(FormItemContext); 47 | const { getFieldState, formState } = useFormContext(); 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState); 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within "); 53 | } 54 | 55 | const { id } = itemContext; 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | }; 65 | }; 66 | 67 | type FormItemContextValue = { 68 | id: string; 69 | }; 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue, 73 | ); 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId(); 80 | 81 | return ( 82 | 83 |
84 | 85 | ); 86 | }); 87 | FormItem.displayName = "FormItem"; 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField(); 94 | 95 | return ( 96 |