diff --git a/.gitignore b/.gitignore
index 72f09694..cb1a1417 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,9 @@ dist-ssr
# Coverage
coverage
+# Firebase
+.firebase
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/eslint.config.ts b/eslint.config.ts
index 7b9b527a..eddd6d89 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -20,6 +20,7 @@ const config: any[] = [
"**/shadcn/public-dev/**",
"packages/styles/dist.css",
"packages/angular/**",
+ "packages/shadcn/public",
]),
...tseslint.configs.recommended,
{
@@ -48,7 +49,7 @@ const config: any[] = [
},
{
// React package specific rules
- files: ["packages/react/src/**/*.{ts,tsx}"],
+ files: ["packages/react/src/**/*.{ts,tsx}", "packages/shadcn/src/**/*.{ts,tsx}"],
plugins: { react: pluginReact, "react-hooks": pluginReactHooks },
languageOptions: {
parserOptions: {
diff --git a/examples/shadcn/.gitignore b/examples/shadcn/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/examples/shadcn/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/shadcn/README.md b/examples/shadcn/README.md
new file mode 100644
index 00000000..30404ce4
--- /dev/null
+++ b/examples/shadcn/README.md
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/examples/shadcn/add-all.ts b/examples/shadcn/add-all.ts
new file mode 100644
index 00000000..b295913c
--- /dev/null
+++ b/examples/shadcn/add-all.ts
@@ -0,0 +1,39 @@
+import parser from "yargs-parser";
+import readline from "node:readline";
+import registryJson from "../../packages/shadcn/registry-spec.json";
+import { execSync } from "node:child_process";
+
+const components = registryJson.items.map((item) => item.name);
+const args = parser(process.argv.slice(2));
+const prefix = String(args.prefix) || "@dev";
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+const items = components
+ .map((component) => {
+ return `${prefix}/${component}`;
+ })
+ .join(" ");
+
+rl.question(
+ `Add ${components.length} components. This will overrwrite all existing files. Continue? (y/N) `,
+ (answer: unknown) => {
+ const answerString = String(answer || "n").toLowerCase();
+
+ if (answerString === "y") {
+ try {
+ execSync(`pnpm dlx shadcn@latest add -y -o -a ${items}`, { stdio: "inherit" });
+ process.exit(0);
+ } catch (error) {
+ console.error(error);
+ process.exit(1);
+ }
+ }
+
+ console.log("Aborting...");
+ process.exit(0);
+ }
+);
diff --git a/examples/shadcn/components.json b/examples/shadcn/components.json
new file mode 100644
index 00000000..ea1fd790
--- /dev/null
+++ b/examples/shadcn/components.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {
+ "@dev": "http://localhost:5178/{name}.json",
+ "@firebase-ui": "https://ui.firebase.com/{name}.json"
+ }
+}
diff --git a/examples/shadcn/index.html b/examples/shadcn/index.html
new file mode 100644
index 00000000..1d79ade8
--- /dev/null
+++ b/examples/shadcn/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ shadcn
+
+
+
+
+
+
+
diff --git a/examples/shadcn/package.json b/examples/shadcn/package.json
new file mode 100644
index 00000000..654e7976
--- /dev/null
+++ b/examples/shadcn/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "shadcn",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "shadcn:add-all": "tsx add-all.ts"
+ },
+ "dependencies": {
+ "@firebase-ui/core": "workspace:*",
+ "@firebase-ui/react": "workspace:*",
+ "@firebase-ui/styles": "workspace:*",
+ "@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-aspect-ratio": "^1.1.7",
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-collapsible": "^1.1.12",
+ "@radix-ui/react-context-menu": "^2.2.16",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-hover-card": "^1.1.15",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-menubar": "^1.1.16",
+ "@radix-ui/react-navigation-menu": "^1.2.14",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-progress": "^1.1.7",
+ "@radix-ui/react-radio-group": "^1.3.8",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slider": "^1.3.6",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toggle": "^1.1.10",
+ "@radix-ui/react-toggle-group": "^1.1.11",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "date-fns": "^4.1.0",
+ "embla-carousel-react": "^8.6.0",
+ "firebase": "catalog:",
+ "input-otp": "^1.4.2",
+ "lucide-react": "^0.544.0",
+ "next-themes": "^0.4.6",
+ "react": "catalog:",
+ "react-day-picker": "^9.11.1",
+ "react-dom": "catalog:",
+ "react-hook-form": "^7.64.0",
+ "react-resizable-panels": "^3.0.6",
+ "react-router": "^7.9.3",
+ "react-router-dom": "^6.28.0",
+ "recharts": "2.15.4",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.3.1",
+ "vaul": "^1.1.2",
+ "zod": "catalog:"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "catalog:",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@types/yargs-parser": "^21.0.3",
+ "@vitejs/plugin-react": "catalog:",
+ "tailwindcss": "catalog:",
+ "tsx": "^4.20.6",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "yargs-parser": "^22.0.0"
+ }
+}
diff --git a/examples/shadcn/public/vite.svg b/examples/shadcn/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/examples/shadcn/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/shadcn/src/App.tsx b/examples/shadcn/src/App.tsx
new file mode 100644
index 00000000..3ef015d3
--- /dev/null
+++ b/examples/shadcn/src/App.tsx
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NavLink } from "react-router";
+import { useUser } from "./firebase/hooks";
+
+function App() {
+ const user = useUser();
+
+ return (
+
+
Firebase UI Demo
+
{user &&
Welcome: {user.email || user.phoneNumber}
}
+
+
Auth Screens
+
+
+
+ Sign In Auth Screen
+
+
+
+
+ Sign In Auth Screen with Handlers
+
+
+
+
+ Sign In Auth Screen with OAuth
+
+
+
+
+ Email Link Auth Screen
+
+
+
+
+ Email Link Auth Screen with OAuth
+
+
+
+
+ Phone Auth Screen
+
+
+
+
+ Phone Auth Screen with OAuth
+
+
+
+
+ Sign Up Auth Screen
+
+
+
+
+ Sign Up Auth Screen with OAuth
+
+
+
+
+ OAuth Screen
+
+
+
+
+ Password Reset Screen
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/examples/shadcn/src/assets/react.svg b/examples/shadcn/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/examples/shadcn/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/shadcn/src/components/email-link-auth-form.tsx b/examples/shadcn/src/components/email-link-auth-form.tsx
new file mode 100644
index 00000000..b51c077c
--- /dev/null
+++ b/examples/shadcn/src/components/email-link-auth-form.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import type { EmailLinkAuthFormSchema } from "@firebase-ui/core";
+import {
+ useEmailLinkAuthFormAction,
+ useEmailLinkAuthFormCompleteSignIn,
+ useEmailLinkAuthFormSchema,
+ useUI,
+ type EmailLinkAuthFormProps,
+} from "@firebase-ui/react";
+
+import { FirebaseUIError, getTranslation } from "@firebase-ui/core";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { Button } from "@/components/ui/button";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Policies } from "./policies";
+
+export type { EmailLinkAuthFormProps };
+
+export function EmailLinkAuthForm(props: EmailLinkAuthFormProps) {
+ const { onEmailSent, onSignIn } = props;
+ const ui = useUI();
+ const schema = useEmailLinkAuthFormSchema();
+ const action = useEmailLinkAuthFormAction();
+ const [emailSent, setEmailSent] = useState(false);
+
+ const form = useForm({
+ resolver: standardSchemaResolver(schema),
+ defaultValues: {
+ email: "",
+ },
+ });
+
+ useEmailLinkAuthFormCompleteSignIn(onSignIn);
+
+ async function onSubmit(values: EmailLinkAuthFormSchema) {
+ try {
+ await action(values);
+ setEmailSent(true);
+ onEmailSent?.();
+ } catch (error) {
+ const message = error instanceof FirebaseUIError ? error.message : String(error);
+ form.setError("root", { message });
+ }
+ }
+
+ if (emailSent) {
+ return (
+
+
{getTranslation(ui, "messages", "signInLinkSent")}
+
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/email-link-auth-screen.tsx b/examples/shadcn/src/components/email-link-auth-screen.tsx
new file mode 100644
index 00000000..b636fe9d
--- /dev/null
+++ b/examples/shadcn/src/components/email-link-auth-screen.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { getTranslation } from "@firebase-ui/core";
+import { useUI, type EmailLinkAuthScreenProps } from "@firebase-ui/react";
+
+import { EmailLinkAuthForm } from "@/components/email-link-auth-form";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+
+export type { EmailLinkAuthScreenProps };
+
+export function EmailLinkAuthScreen({ children, ...props }: EmailLinkAuthScreenProps) {
+ const ui = useUI();
+
+ const titleText = getTranslation(ui, "labels", "signIn");
+ const subtitleText = getTranslation(ui, "prompts", "signInToAccount");
+
+ return (
+
+
+
+ {titleText}
+ {subtitleText}
+
+
+
+ {children ? (
+ <>
+ {getTranslation(ui, "messages", "dividerOr")}
+ {children}
+ >
+ ) : null}
+
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/forgot-password-auth-form.tsx b/examples/shadcn/src/components/forgot-password-auth-form.tsx
new file mode 100644
index 00000000..e050d262
--- /dev/null
+++ b/examples/shadcn/src/components/forgot-password-auth-form.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import type { ForgotPasswordAuthFormSchema } from "@firebase-ui/core";
+import {
+ useForgotPasswordAuthFormAction,
+ useForgotPasswordAuthFormSchema,
+ useUI,
+ type ForgotPasswordAuthFormProps,
+} from "@firebase-ui/react";
+import { useForm } from "react-hook-form";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+import { FirebaseUIError, getTranslation } from "@firebase-ui/core";
+import { useState } from "react";
+
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Policies } from "./policies";
+
+export type { ForgotPasswordAuthFormProps };
+
+export function ForgotPasswordAuthForm(props: ForgotPasswordAuthFormProps) {
+ const ui = useUI();
+ const schema = useForgotPasswordAuthFormSchema();
+ const action = useForgotPasswordAuthFormAction();
+ const [emailSent, setEmailSent] = useState(false);
+
+ const form = useForm({
+ resolver: standardSchemaResolver(schema),
+ defaultValues: {
+ email: "",
+ },
+ });
+
+ async function onSubmit(values: ForgotPasswordAuthFormSchema) {
+ try {
+ await action(values);
+ setEmailSent(true);
+ props.onPasswordSent?.();
+ } catch (error) {
+ const message = error instanceof FirebaseUIError ? error.message : String(error);
+ form.setError("root", { message });
+ }
+ }
+
+ if (emailSent) {
+ return (
+
+
{getTranslation(ui, "messages", "checkEmailForReset")}
+
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/forgot-password-auth-screen.tsx b/examples/shadcn/src/components/forgot-password-auth-screen.tsx
new file mode 100644
index 00000000..cb074b2f
--- /dev/null
+++ b/examples/shadcn/src/components/forgot-password-auth-screen.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { getTranslation } from "@firebase-ui/core";
+import { useUI, type ForgotPasswordAuthScreenProps } from "@firebase-ui/react";
+
+import { ForgotPasswordAuthForm } from "@/components/forgot-password-auth-form";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+
+export type { ForgotPasswordAuthScreenProps };
+
+export function ForgotPasswordAuthScreen(props: ForgotPasswordAuthScreenProps) {
+ const ui = useUI();
+
+ const titleText = getTranslation(ui, "labels", "resetPassword");
+ const subtitleText = getTranslation(ui, "prompts", "enterEmailToReset");
+
+ return (
+
+
+
+ {titleText}
+ {subtitleText}
+
+
+
+
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/google-sign-in-button.tsx b/examples/shadcn/src/components/google-sign-in-button.tsx
new file mode 100644
index 00000000..775234ac
--- /dev/null
+++ b/examples/shadcn/src/components/google-sign-in-button.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { GoogleAuthProvider } from "firebase/auth";
+import { getTranslation } from "@firebase-ui/core";
+import { useUI, type GoogleSignInButtonProps, GoogleLogo } from "@firebase-ui/react";
+
+import { OAuthButton } from "@/components";
+
+export type { GoogleSignInButtonProps };
+
+export function GoogleSignInButton({ provider, themed }: GoogleSignInButtonProps) {
+ const ui = useUI();
+
+ return (
+
+
+ {getTranslation(ui, "labels", "signInWithGoogle")}
+
+ );
+}
diff --git a/examples/shadcn/src/components/header.tsx b/examples/shadcn/src/components/header.tsx
new file mode 100644
index 00000000..5f231c30
--- /dev/null
+++ b/examples/shadcn/src/components/header.tsx
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use client";
+
+import { NavLink } from "react-router";
+import { useUser } from "../firebase/hooks";
+import { signOut } from "firebase/auth";
+import { auth } from "../firebase/firebase";
+
+export function Header() {
+ const user = useUser();
+
+ async function onSignOut() {
+ await signOut(auth);
+ // TODO: Use the router instead of window.location.href
+ window.location.href = "/";
+ }
+
+ return (
+
+
+
+ FirebaseUI
+
+
+
+ {user ? (
+
+ Sign Out
+
+ ) : (
+
+ Sign In
+
+ )}
+
+
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/oauth-button.tsx b/examples/shadcn/src/components/oauth-button.tsx
new file mode 100644
index 00000000..dd5d3740
--- /dev/null
+++ b/examples/shadcn/src/components/oauth-button.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useUI, type OAuthButtonProps, useSignInWithProvider } from "@firebase-ui/react";
+import { Button } from "@/components/ui/button";
+
+export type { OAuthButtonProps };
+
+export function OAuthButton({ provider, children, themed }: OAuthButtonProps) {
+ const ui = useUI();
+
+ const { error, callback } = useSignInWithProvider(provider);
+
+ return (
+
+
+ {children}
+
+ {error &&
{error}
}
+
+ );
+}
diff --git a/examples/shadcn/src/components/phone-auth-form.tsx b/examples/shadcn/src/components/phone-auth-form.tsx
new file mode 100644
index 00000000..05dc2959
--- /dev/null
+++ b/examples/shadcn/src/components/phone-auth-form.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import type { PhoneAuthNumberFormSchema } from "@firebase-ui/core";
+import { FirebaseUIError, getTranslation } from "@firebase-ui/core";
+import {
+ type PhoneAuthFormProps,
+ usePhoneAuthNumberFormSchema,
+ usePhoneNumberFormAction,
+ useUI,
+} from "@firebase-ui/react";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+import { useForm } from "react-hook-form";
+
+import { Button } from "@/components/ui/button";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Policies } from "./policies";
+
+export type { PhoneAuthFormProps };
+
+export function PhoneAuthForm(props: PhoneAuthFormProps) {
+ const ui = useUI();
+ const schema = usePhoneAuthNumberFormSchema();
+ const action = usePhoneNumberFormAction();
+
+ const form = useForm({
+ resolver: standardSchemaResolver(schema),
+ defaultValues: {
+ phoneNumber: "",
+ },
+ });
+
+ async function onSubmit(values: PhoneAuthNumberFormSchema) {
+ try {
+ const credential = await action(values);
+ props.onSignIn?.(credential);
+ } catch (error) {
+ const message = error instanceof FirebaseUIError ? error.message : String(error);
+ form.setError("root", { message });
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/policies.tsx b/examples/shadcn/src/components/policies.tsx
new file mode 100644
index 00000000..970cf37f
--- /dev/null
+++ b/examples/shadcn/src/components/policies.tsx
@@ -0,0 +1,50 @@
+import { cn } from "@/lib/utils";
+import { getTranslation } from "@firebase-ui/core";
+import { useUI, PolicyContext } from "@firebase-ui/react";
+import { cloneElement, useContext } from "react";
+
+export function Policies() {
+ const ui = useUI();
+ const policies = useContext(PolicyContext);
+
+ if (!policies) {
+ return null;
+ }
+
+ const { termsOfServiceUrl, privacyPolicyUrl, onNavigate } = policies;
+ const termsAndPrivacyText = getTranslation(ui, "messages", "termsAndPrivacy");
+ const parts = termsAndPrivacyText.split(/(\{tos\}|\{privacy\})/);
+
+ const className = cn("hover:underline font-semibold");
+ const Handler = onNavigate ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {parts.map((part: string, index: number) => {
+ if (part === "{tos}") {
+ return cloneElement(Handler, {
+ key: index,
+ onClick: onNavigate ? () => onNavigate(termsOfServiceUrl) : undefined,
+ href: onNavigate ? undefined : termsOfServiceUrl,
+ children: getTranslation(ui, "labels", "termsOfService"),
+ });
+ }
+
+ if (part === "{privacy}") {
+ return cloneElement(Handler, {
+ key: index,
+ onClick: onNavigate ? () => onNavigate(privacyPolicyUrl) : undefined,
+ href: onNavigate ? undefined : privacyPolicyUrl,
+ children: getTranslation(ui, "labels", "privacyPolicy"),
+ });
+ }
+
+ return {part} ;
+ })}
+
+ );
+}
diff --git a/examples/shadcn/src/components/sign-in-auth-form.tsx b/examples/shadcn/src/components/sign-in-auth-form.tsx
new file mode 100644
index 00000000..93c2bef6
--- /dev/null
+++ b/examples/shadcn/src/components/sign-in-auth-form.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import type { SignInAuthFormSchema } from "@firebase-ui/core";
+import { useSignInAuthFormAction, useSignInAuthFormSchema, useUI, type SignInAuthFormProps } from "@firebase-ui/react";
+import { useForm } from "react-hook-form";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+import { FirebaseUIError, getTranslation } from "@firebase-ui/core";
+
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Policies } from "./policies";
+
+export type { SignInAuthFormProps };
+
+export function SignInAuthForm(props: SignInAuthFormProps) {
+ const ui = useUI();
+ const schema = useSignInAuthFormSchema();
+ const action = useSignInAuthFormAction();
+
+ const form = useForm({
+ resolver: standardSchemaResolver(schema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ async function onSubmit(values: SignInAuthFormSchema) {
+ try {
+ const credential = await action(values);
+ props.onSignIn?.(credential);
+ } catch (error) {
+ const message = error instanceof FirebaseUIError ? error.message : String(error);
+ form.setError("root", { message });
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/sign-in-auth-screen.tsx b/examples/shadcn/src/components/sign-in-auth-screen.tsx
new file mode 100644
index 00000000..0b6cf4ea
--- /dev/null
+++ b/examples/shadcn/src/components/sign-in-auth-screen.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { getTranslation } from "@firebase-ui/core";
+import { useUI, type SignInAuthScreenProps } from "@firebase-ui/react";
+
+import { SignInAuthForm } from "@/components/sign-in-auth-form";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+
+export type { SignInAuthScreenProps };
+
+export function SignInAuthScreen({ children, ...props }: SignInAuthScreenProps) {
+ const ui = useUI();
+
+ const titleText = getTranslation(ui, "labels", "signIn");
+ const subtitleText = getTranslation(ui, "prompts", "signInToAccount");
+
+ return (
+
+
+
+ {titleText}
+ {subtitleText}
+
+
+
+ {children ? (
+ <>
+ {getTranslation(ui, "messages", "dividerOr")}
+ {children}
+ >
+ ) : null}
+
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/sign-up-auth-form.tsx b/examples/shadcn/src/components/sign-up-auth-form.tsx
new file mode 100644
index 00000000..dc8ea78c
--- /dev/null
+++ b/examples/shadcn/src/components/sign-up-auth-form.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import type { SignUpAuthFormSchema } from "@firebase-ui/core";
+import {
+ useSignUpAuthFormAction,
+ useSignUpAuthFormSchema,
+ useUI,
+ type SignUpAuthFormProps,
+ useRequireDisplayName,
+} from "@firebase-ui/react";
+import { useForm } from "react-hook-form";
+import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
+import { FirebaseUIError, getTranslation } from "@firebase-ui/core";
+
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Policies } from "./policies";
+
+export type { SignUpAuthFormProps };
+
+export function SignUpAuthForm(props: SignUpAuthFormProps) {
+ const ui = useUI();
+ const schema = useSignUpAuthFormSchema();
+ const action = useSignUpAuthFormAction();
+ const requireDisplayName = useRequireDisplayName();
+
+ const form = useForm({
+ resolver: standardSchemaResolver(schema),
+ defaultValues: {
+ email: "",
+ password: "",
+ displayName: requireDisplayName ? "" : undefined,
+ },
+ });
+
+ async function onSubmit(values: SignUpAuthFormSchema) {
+ try {
+ const credential = await action(values);
+ props.onSignUp?.(credential);
+ } catch (error) {
+ const message = error instanceof FirebaseUIError ? error.message : String(error);
+ form.setError("root", { message });
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/sign-up-auth-screen.tsx b/examples/shadcn/src/components/sign-up-auth-screen.tsx
new file mode 100644
index 00000000..e12daf3a
--- /dev/null
+++ b/examples/shadcn/src/components/sign-up-auth-screen.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { getTranslation } from "@firebase-ui/core";
+import { useUI, type SignUpAuthScreenProps } from "@firebase-ui/react";
+
+import { SignUpAuthForm } from "@/components/sign-up-auth-form";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+
+export type { SignUpAuthScreenProps };
+
+export function SignUpAuthScreen({ children, ...props }: SignUpAuthScreenProps) {
+ const ui = useUI();
+
+ const titleText = getTranslation(ui, "labels", "register");
+ const subtitleText = getTranslation(ui, "prompts", "enterDetailsToCreate");
+
+ return (
+
+
+
+ {titleText}
+ {subtitleText}
+
+
+
+ {children ? (
+ <>
+ {getTranslation(ui, "messages", "dividerOr")}
+ {children}
+ >
+ ) : null}
+
+
+
+ );
+}
diff --git a/examples/shadcn/src/components/ui/accordion.tsx b/examples/shadcn/src/components/ui/accordion.tsx
new file mode 100644
index 00000000..14d143e0
--- /dev/null
+++ b/examples/shadcn/src/components/ui/accordion.tsx
@@ -0,0 +1,51 @@
+import * as React from "react";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDownIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Accordion({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function AccordionItem({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/examples/shadcn/src/components/ui/alert-dialog.tsx b/examples/shadcn/src/components/ui/alert-dialog.tsx
new file mode 100644
index 00000000..271c9cff
--- /dev/null
+++ b/examples/shadcn/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,111 @@
+import * as React from "react";
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+function AlertDialog({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function AlertDialogTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function AlertDialogPortal({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function AlertDialogOverlay({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogContent({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+ );
+}
+
+function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogAction({ className, ...props }: React.ComponentProps) {
+ return ;
+}
+
+function AlertDialogCancel({ className, ...props }: React.ComponentProps) {
+ return ;
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/examples/shadcn/src/components/ui/alert.tsx b/examples/shadcn/src/components/ui/alert.tsx
new file mode 100644
index 00000000..c6f7846f
--- /dev/null
+++ b/examples/shadcn/src/components/ui/alert.tsx
@@ -0,0 +1,49 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps) {
+ return
;
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/examples/shadcn/src/components/ui/aspect-ratio.tsx b/examples/shadcn/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 00000000..59d6b357
--- /dev/null
+++ b/examples/shadcn/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
+
+function AspectRatio({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+export { AspectRatio };
diff --git a/examples/shadcn/src/components/ui/avatar.tsx b/examples/shadcn/src/components/ui/avatar.tsx
new file mode 100644
index 00000000..83b8ec6b
--- /dev/null
+++ b/examples/shadcn/src/components/ui/avatar.tsx
@@ -0,0 +1,32 @@
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "@/lib/utils";
+
+function Avatar({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarImage({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarFallback({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/examples/shadcn/src/components/ui/badge.tsx b/examples/shadcn/src/components/ui/badge.tsx
new file mode 100644
index 00000000..a6b124f7
--- /dev/null
+++ b/examples/shadcn/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span";
+
+ return ;
+}
+
+export { Badge, badgeVariants };
diff --git a/examples/shadcn/src/components/ui/breadcrumb.tsx b/examples/shadcn/src/components/ui/breadcrumb.tsx
new file mode 100644
index 00000000..ce08888f
--- /dev/null
+++ b/examples/shadcn/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,92 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return ;
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ );
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return ;
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean;
+}) {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+
+ );
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ );
+}
+
+function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ );
+}
+
+function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ );
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/examples/shadcn/src/components/ui/button-group.tsx b/examples/shadcn/src/components/ui/button-group.tsx
new file mode 100644
index 00000000..0621505f
--- /dev/null
+++ b/examples/shadcn/src/components/ui/button-group.tsx
@@ -0,0 +1,75 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { Separator } from "@/components/ui/separator";
+
+const buttonGroupVariants = cva(
+ "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
+ vertical:
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
+ },
+ },
+ defaultVariants: {
+ orientation: "horizontal",
+ },
+ }
+);
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ );
+}
+
+function ButtonGroupText({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ asChild?: boolean;
+}) {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+
+ );
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };
diff --git a/examples/shadcn/src/components/ui/button.tsx b/examples/shadcn/src/components/ui/button.tsx
new file mode 100644
index 00000000..1ee14790
--- /dev/null
+++ b/examples/shadcn/src/components/ui/button.tsx
@@ -0,0 +1,52 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : "button";
+
+ return ;
+}
+
+export { Button, buttonVariants };
diff --git a/examples/shadcn/src/components/ui/calendar.tsx b/examples/shadcn/src/components/ui/calendar.tsx
new file mode 100644
index 00000000..caaaf7ce
--- /dev/null
+++ b/examples/shadcn/src/components/ui/calendar.tsx
@@ -0,0 +1,158 @@
+"use client";
+
+import * as React from "react";
+import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
+
+import { cn } from "@/lib/utils";
+import { Button, buttonVariants } from "@/components/ui/button";
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = "label",
+ buttonVariant = "ghost",
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps["variant"];
+}) {
+ const defaultClassNames = getDefaultClassNames();
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn("w-fit", defaultClassNames.root),
+ months: cn("flex gap-4 flex-col md:flex-row relative", defaultClassNames.months),
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
+ nav: cn("flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", defaultClassNames.nav),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+ defaultClassNames.button_previous
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+ defaultClassNames.button_next
+ ),
+ month_caption: cn(
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
+ defaultClassNames.month_caption
+ ),
+ dropdowns: cn(
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
+ defaultClassNames.dropdowns
+ ),
+ dropdown_root: cn(
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
+ defaultClassNames.dropdown_root
+ ),
+ dropdown: cn("absolute bg-popover inset-0 opacity-0", defaultClassNames.dropdown),
+ caption_label: cn(
+ "select-none font-medium",
+ captionLayout === "label"
+ ? "text-sm"
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
+ defaultClassNames.caption_label
+ ),
+ table: "w-full border-collapse",
+ weekdays: cn("flex", defaultClassNames.weekdays),
+ weekday: cn(
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
+ defaultClassNames.weekday
+ ),
+ week: cn("flex w-full mt-2", defaultClassNames.week),
+ week_number_header: cn("select-none w-(--cell-size)", defaultClassNames.week_number_header),
+ week_number: cn("text-[0.8rem] select-none text-muted-foreground", defaultClassNames.week_number),
+ day: cn(
+ "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
+ defaultClassNames.day
+ ),
+ range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
+ today: cn(
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
+ defaultClassNames.today
+ ),
+ outside: cn("text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside),
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
+ hidden: cn("invisible", defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return
;
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return ;
+ }
+
+ if (orientation === "right") {
+ return ;
+ }
+
+ return ;
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+ {children}
+
+ );
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ );
+}
+
+function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames();
+
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus();
+ }, [modifiers.focused]);
+
+ return (
+ span]:text-xs [&>span]:opacity-70",
+ defaultClassNames.day,
+ className
+ )}
+ {...props}
+ />
+ );
+}
+
+export { Calendar, CalendarDayButton };
diff --git a/examples/shadcn/src/components/ui/card.tsx b/examples/shadcn/src/components/ui/card.tsx
new file mode 100644
index 00000000..9939da87
--- /dev/null
+++ b/examples/shadcn/src/components/ui/card.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
diff --git a/examples/shadcn/src/components/ui/carousel.tsx b/examples/shadcn/src/components/ui/carousel.tsx
new file mode 100644
index 00000000..823f83a3
--- /dev/null
+++ b/examples/shadcn/src/components/ui/carousel.tsx
@@ -0,0 +1,214 @@
+import * as React from "react";
+import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ");
+ }
+
+ return context;
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return;
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext]
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) return;
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) return;
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
+
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+
+ Previous slide
+
+ );
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+
+ Next slide
+
+ );
+}
+
+export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
diff --git a/examples/shadcn/src/components/ui/chart.tsx b/examples/shadcn/src/components/ui/chart.tsx
new file mode 100644
index 00000000..5547ef14
--- /dev/null
+++ b/examples/shadcn/src/components/ui/chart.tsx
@@ -0,0 +1,298 @@
+"use client";
+
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & ({ color?: string; theme?: never } | { color?: never; theme: Record });
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps["children"];
+}) {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+ return (
+
+
+
+ {children}
+
+
+ );
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+