├── app
├── favicon.ico
├── robots.ts
├── sitemap.ts
├── (home)
│ ├── page.tsx
│ └── sections
│ │ ├── variants.tsx
│ │ ├── setup.tsx
│ │ └── hero.tsx
├── layout.tsx
└── globals.css
├── postcss.config.mjs
├── public
├── vercel.svg
├── window.svg
├── file.svg
├── globe.svg
├── next.svg
├── r
│ └── font-picker.json
└── startup-listing.svg
├── lib
├── utils.ts
└── fonts.ts
├── types
└── index.d.ts
├── next.config.ts
├── components
├── ui
│ ├── skeleton.tsx
│ ├── sonner.tsx
│ ├── label.tsx
│ ├── input.tsx
│ ├── hover-card.tsx
│ ├── badge.tsx
│ ├── popover.tsx
│ ├── scroll-area.tsx
│ ├── tabs.tsx
│ ├── accordion.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── form.tsx
│ ├── command.tsx
│ ├── select.tsx
│ ├── dropdown-menu.tsx
│ └── font-picker.tsx
├── providers.tsx
├── snippet.tsx
├── code-block.tsx
├── theme-toggle.tsx
├── pre.tsx
├── copy-button.tsx
├── footer.tsx
└── header.tsx
├── config
└── site.ts
├── eslint.config.mjs
├── components.json
├── .gitignore
├── prettier.config.mjs
├── registry.json
├── tsconfig.json
├── contentlayer.config.ts
├── LICENSE.md
├── hooks
└── use-copy.tsx
├── .commitlintrc.js
├── package.json
├── README.md
└── content
└── snippets
├── fonts.mdx
└── font-picker.mdx
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevinodpatidar/shadcn-font-picker/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export type SiteConfig = {
2 | name: string;
3 | description: string;
4 | url: string;
5 | links: {
6 | twitter: string;
7 | github: string;
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | const { withContentlayer } = require("next-contentlayer");
2 |
3 | /** @type {import('next').NextConfig} */
4 | const nextConfig = { reactStrictMode: true, swcMinify: true };
5 |
6 | module.exports = withContentlayer(nextConfig);
7 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next";
2 |
3 | import { siteConfig } from "../config/site";
4 |
5 | export default function robots(): MetadataRoute.Robots {
6 | return {
7 | rules: {
8 | userAgent: "*",
9 | allow: "/",
10 | disallow: "/private/",
11 | },
12 | sitemap: `${siteConfig.url}/sitemap.xml`,
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | import { SiteConfig } from "@/types";
2 |
3 | export const siteConfig: SiteConfig = {
4 | name: "Shadcn Font Picker",
5 | description:
6 | "A font picker component implementation of Shadcn's input component",
7 | url: "https://shadcn-font-picker.vercel.app",
8 | links: {
9 | twitter: "https://twitter.com/thevinodpatidar",
10 | github: "https://github.com/thevinodpatidar/shadcn-font-picker",
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/components/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider } from "next-themes";
4 |
5 | export function Providers({ children }: { children: React.ReactNode }) {
6 |
7 | return (
8 |
15 | {children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/components/snippet.tsx:
--------------------------------------------------------------------------------
1 |
2 | "use client";
3 |
4 | import React from "react";
5 | import { useMDXComponent } from "next-contentlayer/hooks";
6 |
7 | import type { Snippet as SnippetType } from ".contentlayer/generated";
8 | import Pre from "./pre";
9 |
10 | const components = {
11 | pre: Pre,
12 | };
13 |
14 | export function Snippet({ snippet }: { snippet: SnippetType }) {
15 | const MDXContent = useMDXComponent(snippet.body.code);
16 | return ;
17 | }
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
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 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/.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 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | # contentlayer
38 | .contentlayer
--------------------------------------------------------------------------------
/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { type MetadataRoute } from "next";
2 |
3 | import { siteConfig } from "../config/site";
4 |
5 | export default function sitemap(): MetadataRoute.Sitemap {
6 | return [
7 | {
8 | url: `${siteConfig.url}/`,
9 | lastModified: new Date(),
10 | },
11 | {
12 | url: `${siteConfig.url}/#try`,
13 | lastModified: new Date(),
14 | },
15 | {
16 | url: `${siteConfig.url}/#setup`,
17 | lastModified: new Date(),
18 | },
19 | {
20 | url: `${siteConfig.url}/#variants`,
21 | lastModified: new Date(),
22 | },
23 | ];
24 | }
25 |
--------------------------------------------------------------------------------
/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | /** @typedef {import("prettier").Config} PrettierConfig */
2 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */
3 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
4 |
5 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */
6 | const config = {
7 | plugins: [
8 | "@ianvs/prettier-plugin-sort-imports",
9 | "prettier-plugin-tailwindcss",
10 | ],
11 | tailwindFunctions: ["cn", "cva"],
12 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
13 | importOrderTypeScriptVersion: "4.4.0",
14 | };
15 |
16 | export default config;
17 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner, ToasterProps } from "sonner"
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
22 | )
23 | }
24 |
25 | export { Toaster }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/registry.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry.json",
3 | "name": "shadcn-font-picker",
4 | "homepage": "https://shadcn-font-picker.vercel.app",
5 | "items": [
6 | {
7 | "name": "font-picker",
8 | "type": "registry:block",
9 | "title": "Font Picker",
10 | "description": "A font picker component.",
11 | "registryDependencies": ["button", "command", "dropdown-menu", "popover"],
12 | "dependencies": ["react-window", "@types/react-window"],
13 | "files": [
14 | {
15 | "path": "components/ui/font-picker.tsx",
16 | "type": "registry:component"
17 | },
18 | {
19 | "path": "lib/fonts.ts",
20 | "type": "registry:lib"
21 | }
22 | ]
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/components/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import CopyButton from "./copy-button";
4 |
5 | function CodeBlock({
6 | value,
7 | className,
8 | copyable = true,
9 | }: {
10 | value: string;
11 | className?: string;
12 | codeClass?: string;
13 | copyable?: boolean;
14 | codeWrap?: boolean;
15 | noCodeFont?: boolean;
16 | noMask?: boolean;
17 | }) {
18 | value = value || "";
19 |
20 | return (
21 |
28 |
29 | {value}
30 |
31 | );
32 | }
33 |
34 | export default CodeBlock;
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "baseUrl": ".",
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"],
24 | "contentlayer/generated": ["./.contentlayer/generated"]
25 | }
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "**/*.ts",
30 | "**/*.tsx",
31 | ".next/types/**/*.ts",
32 | ".contentlayer/generated"
33 | ],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/contentlayer.config.ts:
--------------------------------------------------------------------------------
1 | import { defineDocumentType, makeSource } from "contentlayer/source-files";
2 |
3 | export const Snippet = defineDocumentType(() => ({
4 | name: "Snippet",
5 | filePathPattern: `snippets/**/*.mdx`,
6 | contentType: "mdx",
7 | fields: {
8 | file: {
9 | type: "string",
10 | description: "The name of the snippet",
11 | required: true,
12 | },
13 | filePath: {
14 | type: "string",
15 | description: "The path of the snippet",
16 | required: true,
17 | },
18 | order: {
19 | type: "number",
20 | description: "The order of the snippet",
21 | required: true,
22 | },
23 | description: {
24 | type: "string",
25 | description: "The description of the snippet",
26 | required: true,
27 | },
28 | },
29 | computedFields: {
30 | slug: {
31 | type: "string",
32 | resolve: (_) => _._raw.sourceFileName.replace(/\.[^.$]+$/, ""),
33 | },
34 | },
35 | }));
36 |
37 | export default makeSource({
38 | contentDirPath: "content",
39 | documentTypes: [Snippet],
40 | });
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2025 Vinod Patidar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/hooks/use-copy.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | function useClipboard() {
4 | const [isCopied, setIsCopied] = useState(false);
5 |
6 | const copyToClipboard = useCallback(async (text: string) => {
7 | if (navigator.clipboard && window.isSecureContext) {
8 | // Navigator Clipboard API method'
9 | try {
10 | await navigator.clipboard.writeText(text);
11 | setIsCopied(true);
12 | } catch (err) {
13 | console.error(err);
14 | setIsCopied(false);
15 | }
16 | } else {
17 | // Clipboard API not available, use fallback
18 | const textArea = document.createElement("textarea");
19 | textArea.value = text;
20 | document.body.appendChild(textArea);
21 | textArea.focus();
22 | textArea.select();
23 | try {
24 | const successful: boolean = document.execCommand("copy");
25 | setIsCopied(successful);
26 | } catch (err) {
27 | console.error(err);
28 | setIsCopied(false);
29 | }
30 | document.body.removeChild(textArea);
31 | }
32 | }, []);
33 |
34 | return { isCopied, copyToClipboard };
35 | }
36 |
37 | export default useClipboard;
38 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.commitlintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"],
3 | rules: {
4 | "type-enum": [
5 | 2,
6 | "always",
7 | [
8 | // Changes that affect the build system or dependency-only changes
9 | "build",
10 | // Changes to CI workflows
11 | "ci",
12 | // Documentation-only changes
13 | "docs",
14 | // A new feature
15 | "feat",
16 | //A bug fix
17 | "fix",
18 | // A code change that improves performance
19 | "perf",
20 | // A code change that neither fixes a bug nor adds a feature
21 | "refactor",
22 | // A commit that reverts a previous commit
23 | "revert",
24 | // Changes that do not affect the meaning of the code
25 | "style",
26 | // Adding missing tests or correcting existing tests
27 | "test",
28 | ],
29 | ],
30 | "scope-enum": [
31 | 2,
32 | "always",
33 | [
34 | // Dependency-related changes
35 | "deps",
36 | // ESLint-related changes
37 | "eslint",
38 | // Prettier-related changes
39 | "prettier",
40 | // TypeScript-related changes
41 | "typescript",
42 | // Go-related changes
43 | "golang",
44 | ],
45 | ],
46 | "scope-empty": [1, "never"],
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Moon, Sun } from "lucide-react";
4 | import { useTheme } from "next-themes";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu";
13 |
14 | export function ThemeToggle() {
15 | const { setTheme } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
25 |
26 |
27 | setTheme("light")}>
28 | Light
29 |
30 | setTheme("dark")}>
31 | Dark
32 |
33 | setTheme("system")}>
34 | System
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function HoverCard({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function HoverCardTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return (
18 |
19 | )
20 | }
21 |
22 | function HoverCardContent({
23 | className,
24 | align = "center",
25 | sideOffset = 4,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
30 |
40 |
41 | )
42 | }
43 |
44 | export { HoverCard, HoverCardTrigger, HoverCardContent }
45 |
--------------------------------------------------------------------------------
/components/pre.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Check, Copy } from "lucide-react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | import { Button } from "./ui/button";
7 |
8 | export default function Pre({
9 | children,
10 | className,
11 | ...props
12 | }: React.HTMLAttributes) {
13 | const [copied, setCopied] = React.useState(false);
14 | const ref = React.useRef(null);
15 |
16 | React.useEffect(() => {
17 | let timer: ReturnType;
18 | if (copied) {
19 | timer = setTimeout(() => {
20 | setCopied(false);
21 | }, 2000);
22 | }
23 | return () => {
24 | clearTimeout(timer);
25 | };
26 | }, [copied]);
27 |
28 | const onClick = () => {
29 | setCopied(true);
30 | const content = ref.current?.textContent;
31 | if (content) {
32 | navigator.clipboard.writeText(content);
33 | }
34 | };
35 |
36 | return (
37 |
38 |
48 |
56 | {children}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/app/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Hero from "@/app/(home)/sections/hero";
4 | import Setup from "@/app/(home)/sections/setup";
5 | import { Header } from "@/components/header";
6 | import { siteConfig } from "@/config/site";
7 | import Variants from "./sections/variants";
8 |
9 | export default function Home() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
48 |
49 | );
50 | }
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md 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",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "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",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
49 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function ScrollArea({
9 | className,
10 | children,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 |
23 | {children}
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function ScrollBar({
32 | className,
33 | orientation = "vertical",
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
50 |
54 |
55 | )
56 | }
57 |
58 | export { ScrollArea, ScrollBar }
59 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Providers } from "@/components/providers";
2 | import { Toaster } from "@/components/ui/sonner";
3 | import type { Metadata } from "next";
4 | import { Geist, Geist_Mono } from "next/font/google";
5 | import "./globals.css";
6 |
7 | const geistSans = Geist({
8 | variable: "--font-geist-sans",
9 | subsets: ["latin"],
10 | });
11 |
12 | const geistMono = Geist_Mono({
13 | variable: "--font-geist-mono",
14 | subsets: ["latin"],
15 | });
16 |
17 | export const metadata: Metadata = {
18 | title: "Shadcn Font Picker - Beautiful Font Selection Component",
19 | description:
20 | "A beautiful and customizable font picker component built with shadcn/ui and Google Fonts API. Perfect for modern web applications.",
21 | keywords: [
22 | "shadcn",
23 | "font picker",
24 | "react",
25 | "component",
26 | "google fonts",
27 | "ui",
28 | ],
29 | alternates: { canonical: "https://shadcn-font-picker.vercel.app/" },
30 | authors: [{ name: "Vinod Patidar" }],
31 | openGraph: {
32 | title: "Shadcn Font Picker",
33 | description:
34 | "A beautiful font picker component built with shadcn/ui and Google Fonts API",
35 | type: "website",
36 | url: "https://shadcn-font-picker.vercel.app/",
37 | siteName: "Shadcn Font Picker",
38 | },
39 | twitter: {
40 | card: "summary_large_image",
41 | title: "Shadcn Font Picker",
42 | description:
43 | "A beautiful font picker component built with shadcn/ui and Google Fonts API",
44 | creator: "@thevinodpatidar",
45 | },
46 | icons: { icon: "/favicon.ico" },
47 | verification: {
48 | google: "Hz1IFTnXjR3j5H80jC25eENAjgzEhbatuRNeg46tTow",
49 | },
50 | };
51 |
52 | export default function RootLayout({
53 | children,
54 | }: Readonly<{
55 | children: React.ReactNode;
56 | }>) {
57 | return (
58 |
59 |
62 | {children}
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/components/copy-button.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { AnimatePresence, motion, MotionConfig } from "framer-motion";
3 | import { Check, Copy } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | import { Button } from "./ui/button";
8 |
9 | export default function CopyButton({
10 | value,
11 | }: {
12 | value: string;
13 | copyable?: boolean;
14 | }) {
15 | const [copying, setCopying] = useState(0);
16 |
17 | const onCopy = useCallback(async () => {
18 | try {
19 | await navigator.clipboard.writeText(value);
20 | setCopying((c) => c + 1);
21 | setTimeout(() => {
22 | setCopying((c) => c - 1);
23 | }, 2000);
24 | } catch (err) {
25 | console.error("Failed to copy text: ", err);
26 | }
27 | }, [value]);
28 |
29 | const variants = {
30 | visible: { opacity: 1, scale: 1 },
31 | hidden: { opacity: 0, scale: 0.5 },
32 | };
33 |
34 | return (
35 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Tabs({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function TabsList({
22 | className,
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
34 | )
35 | }
36 |
37 | function TabsTrigger({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | function TabsContent({
54 | className,
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
63 | )
64 | }
65 |
66 | export { Tabs, TabsList, TabsTrigger, TabsContent }
67 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDownIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Accordion({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function AccordionItem({
16 | className,
17 | ...props
18 | }: React.ComponentProps) {
19 | return (
20 |
25 | )
26 | }
27 |
28 | function AccordionTrigger({
29 | className,
30 | children,
31 | ...props
32 | }: React.ComponentProps) {
33 | return (
34 |
35 | svg]:rotate-180",
39 | className
40 | )}
41 | {...props}
42 | >
43 | {children}
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | function AccordionContent({
51 | className,
52 | children,
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
61 | {children}
62 |
63 | )
64 | }
65 |
66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
67 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "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",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Github, Twitter } from "lucide-react";
5 |
6 | export function Footer() {
7 | return (
8 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadcn-font-picker",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "contentlayer build && next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "typecheck": "tsc --noEmit",
12 | "format": "prettier --write .",
13 | "prepare": "husky",
14 | "preinstall": "npx only-allow yarn",
15 | "registry:build": "shadcn build"
16 | },
17 | "dependencies": {
18 | "@hookform/resolvers": "^5.0.1",
19 | "@radix-ui/react-accordion": "^1.2.4",
20 | "@radix-ui/react-dialog": "^1.1.7",
21 | "@radix-ui/react-dropdown-menu": "^2.1.7",
22 | "@radix-ui/react-hover-card": "^1.1.7",
23 | "@radix-ui/react-label": "^2.1.3",
24 | "@radix-ui/react-popover": "^1.1.7",
25 | "@radix-ui/react-scroll-area": "^1.2.4",
26 | "@radix-ui/react-select": "^2.1.7",
27 | "@radix-ui/react-slot": "^1.2.3",
28 | "@radix-ui/react-tabs": "^1.1.4",
29 | "class-variance-authority": "^0.7.1",
30 | "clsx": "^2.1.1",
31 | "cmdk": "^1.1.1",
32 | "contentlayer": "^0.3.4",
33 | "framer-motion": "^12.7.2",
34 | "lucide-react": "^0.488.0",
35 | "next": "15.3.0",
36 | "next-contentlayer": "^0.3.4",
37 | "next-themes": "^0.4.6",
38 | "react": "^19.0.0",
39 | "react-dom": "^19.0.0",
40 | "react-hook-form": "^7.55.0",
41 | "react-window": "^1.8.11",
42 | "shadcn": "^2.4.0-canary.20",
43 | "sonner": "^2.0.3",
44 | "tailwind-merge": "^3.2.0",
45 | "tw-animate-css": "^1.2.5",
46 | "zod": "^3.24.2"
47 | },
48 | "devDependencies": {
49 | "@commitlint/cli": "^19.6.0",
50 | "@commitlint/config-conventional": "^19.6.0",
51 | "@eslint/eslintrc": "^3",
52 | "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
53 | "@tailwindcss/postcss": "^4",
54 | "@types/eslint": "^8.56.7",
55 | "@types/node": "^20",
56 | "@types/react": "^19",
57 | "@types/react-dom": "^19",
58 | "@types/react-window": "^1.8.8",
59 | "@typescript-eslint/eslint-plugin": "^7.6.0",
60 | "@typescript-eslint/parser": "^7.6.0",
61 | "eslint": "^9",
62 | "eslint-config-next": "15.3.0",
63 | "eslint-config-prettier": "^9.1.0",
64 | "eslint-plugin-tailwindcss": "^3.15.1",
65 | "husky": "^9.1.7",
66 | "lint-staged": "^15.2.10",
67 | "postcss": "8.4.38",
68 | "prettier": "^3.3.3",
69 | "prettier-plugin-tailwindcss": "^0.6.9",
70 | "rehype": "^13.0.1",
71 | "rehype-pretty-code": "^0.13.1",
72 | "shiki": "^1.3.0",
73 | "tailwindcss": "^4",
74 | "typescript": "^5",
75 | "unist-builder": "4.0.0",
76 | "unist-util-visit": "^5.0.0"
77 | },
78 | "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
79 | }
80 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { siteConfig } from "@/config/site";
5 | import { Moon, Star, Sun } from "lucide-react";
6 | import { useTheme } from "next-themes";
7 | import Link from "next/link";
8 |
9 |
10 | export function Header() {
11 | const { setTheme, theme } = useTheme();
12 |
13 | const handleThemeToggle = () => {
14 | setTheme(theme === "dark" ? "light" : "dark");
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | F
24 |
25 |
Font Picker
26 |
27 |
28 |
29 |
30 |
41 |
42 |
52 |
63 |
64 |
65 |
66 |
67 | );
68 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google Font Picker Component for shadcn/ui
2 |
3 | A beautiful and functional Google Font picker component built with shadcn/ui. This component allows users to search, filter, and preview Google Fonts directly in their application with optimized performance using virtualized rendering.
4 |
5 | ## Features
6 |
7 | - 🔍 Search fonts by name
8 | - 🗂️ Filter fonts by category (serif, sans-serif, display, handwriting, monospace)
9 | - 👀 Live font preview with smooth loading
10 | - 📱 Responsive design
11 | - ♿ Accessible UI components
12 | - 🎨 Customizable styling
13 | - ⚡ Virtualized list rendering for optimal performance
14 | - 🎯 Configurable dimensions and appearance
15 |
16 | ## Prerequisites
17 |
18 | Before using this component, make sure you have:
19 |
20 | 1. A Next.js project with shadcn/ui set up
21 | 2. A Google Fonts API key (get one from [Google Cloud Console](https://console.cloud.google.com/))
22 |
23 | ## Installation
24 |
25 | 1. Add the required dependencies:
26 |
27 | ```bash
28 | npm install lucide-react react-window
29 | ```
30 |
31 | 2. Install the required shadcn/ui components:
32 |
33 | ```bash
34 | npx shadcn-ui@latest add button command dropdown-menu popover
35 | ```
36 |
37 | 3. Set up your environment variables by creating a `.env.local` file:
38 |
39 | ```env
40 | NEXT_PUBLIC_GOOGLE_FONTS_API_KEY=your_google_fonts_api_key_here
41 | ```
42 |
43 | ## Usage
44 |
45 | 1. Import the FontPicker component:
46 |
47 | ```tsx
48 | import { FontPicker } from "@/components/ui/font-picker";
49 | ```
50 |
51 | 2. Use the FontPicker component:
52 |
53 | ```tsx
54 | export default function MyComponent() {
55 | const [selectedFont, setSelectedFont] = useState();
56 |
57 | return (
58 |
66 | );
67 | }
68 | ```
69 |
70 | ## Component API
71 |
72 | ### FontPicker Props
73 |
74 | | Prop | Type | Default | Description |
75 | | ------------- | ------------------------ | ------- | ------------------------------------------------ |
76 | | `value` | `string` | - | The currently selected font family |
77 | | `onChange` | `(font: string) => void` | - | Callback function called when a font is selected |
78 | | `width` | `number` | 300 | Width of the picker component |
79 | | `height` | `number` | 400 | Height of the picker component |
80 | | `className` | `string` | - | Additional CSS classes for customization |
81 | | `showFilters` | `boolean` | true | Whether to show the category filter |
82 |
83 | ### GoogleFont Type
84 |
85 | ```ts
86 | interface GoogleFont {
87 | family: string;
88 | variants: string[];
89 | subsets: string[];
90 | version: string;
91 | lastModified: string;
92 | files: Record;
93 | category: string;
94 | kind: string;
95 | menu: string;
96 | }
97 | ```
98 |
99 | ## License
100 |
101 | MIT
102 |
--------------------------------------------------------------------------------
/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | export interface GoogleFont {
2 | family: string;
3 | variants: string[];
4 | subsets: string[];
5 | version: string;
6 | lastModified: string;
7 | files: Record;
8 | category: string;
9 | kind: string;
10 | }
11 |
12 | const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY;
13 | const API_URL = "https://www.googleapis.com/webfonts/v1/webfonts";
14 |
15 | // Cache for loaded font stylesheets
16 | const loadedFonts = new Set();
17 |
18 | // Cache for the Google Fonts API response
19 | let fontsCache: GoogleFont[] | null = null;
20 | let fontsCacheTimestamp: number | null = null;
21 | const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
22 |
23 | export async function fetchGoogleFonts(): Promise {
24 | // Check if we have a valid cache
25 | if (
26 | fontsCache &&
27 | fontsCacheTimestamp &&
28 | Date.now() - fontsCacheTimestamp < CACHE_DURATION
29 | ) {
30 | return fontsCache;
31 | }
32 |
33 | if (!API_KEY) {
34 | throw new Error("Google Fonts API key is not configured");
35 | }
36 |
37 | try {
38 | const response = await fetch(`${API_URL}?key=${API_KEY}&sort=popularity`);
39 | if (!response.ok) {
40 | throw new Error("Failed to fetch Google Fonts");
41 | }
42 | const data = await response.json();
43 | fontsCache = data.items;
44 | fontsCacheTimestamp = Date.now();
45 | return data.items;
46 | } catch (error) {
47 | // If fetch fails and we have a cache, return it even if expired
48 | if (fontsCache) {
49 | return fontsCache;
50 | }
51 | console.error("Error fetching Google Fonts:", error);
52 | throw error;
53 | }
54 | }
55 |
56 | export function getFontUrl(font: GoogleFont, variant = "regular"): string {
57 | const fontFamily = font.family.replace(/\s+/g, "+");
58 | const fontVariant = variant === "regular" ? "400" : variant;
59 | return `https://fonts.googleapis.com/css2?family=${fontFamily}:wght@${fontVariant}&display=swap`;
60 | }
61 |
62 | export async function loadFont(
63 | fontFamily: string,
64 | variant = "regular",
65 | ): Promise {
66 | if (loadedFonts.has(fontFamily)) {
67 | return;
68 | }
69 |
70 | return new Promise((resolve, reject) => {
71 | const link = document.createElement("link");
72 | link.href = getFontUrl({ family: fontFamily } as GoogleFont, variant);
73 | link.rel = "stylesheet";
74 |
75 | link.onload = () => {
76 | loadedFonts.add(fontFamily);
77 | resolve();
78 | };
79 |
80 | link.onerror = () => {
81 | reject(new Error(`Failed to load font: ${fontFamily}`));
82 | };
83 |
84 | document.head.appendChild(link);
85 | });
86 | }
87 |
88 | export interface FontPickerProps {
89 | onFontSelect?: (font: GoogleFont) => void;
90 | value?: string;
91 | }
92 |
93 | export const FONT_CATEGORIES = [
94 | "serif",
95 | "sans-serif",
96 | "display",
97 | "handwriting",
98 | "monospace",
99 | ] as const;
100 |
101 | export type FontCategory = (typeof FONT_CATEGORIES)[number];
102 |
103 | export const FONT_WEIGHTS = [
104 | "100",
105 | "200",
106 | "300",
107 | "400",
108 | "500",
109 | "600",
110 | "700",
111 | "800",
112 | "900",
113 | ] as const;
114 |
115 | export type FontWeight = (typeof FONT_WEIGHTS)[number];
116 |
--------------------------------------------------------------------------------
/app/(home)/sections/variants.tsx:
--------------------------------------------------------------------------------
1 | import { FontPicker } from "@/components/ui/font-picker";
2 | import { useState } from "react";
3 |
4 | export default function Variants() {
5 | const [font, setFont] = useState("");
6 |
7 | return (
8 |
9 |
10 | Variants
11 |
12 |
13 |
14 | The font picker component can be used as different variants.
15 |
16 |
17 |
18 | Default
19 |
20 |
21 | setFont(font)} />
22 |
26 | This is a custom implementation of the Font Picker component.
27 |
28 |
29 |
30 |
31 |
32 | Custom width
33 |
34 |
35 | setFont(font)}
38 | width={200}
39 | />
40 |
41 |
42 |
43 |
44 | Custom height
45 |
46 |
47 | setFont(font)}
50 | height={200}
51 | />
52 |
53 |
54 |
55 |
56 | Without filters
57 |
58 |
59 | setFont(font)}
62 | showFilters={false}
63 | />
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/content/snippets/fonts.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | file: fonts.ts
3 | filePath: lib/fonts.ts
4 | order: 2
5 | description: Use the fonts component
6 | ---
7 |
8 | ```ts
9 | export interface GoogleFont {
10 | family: string;
11 | variants: string[];
12 | subsets: string[];
13 | version: string;
14 | lastModified: string;
15 | files: Record;
16 | category: string;
17 | kind: string;
18 | }
19 |
20 | const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY;
21 | const API_URL = "https://www.googleapis.com/webfonts/v1/webfonts";
22 |
23 | // Cache for loaded font stylesheets
24 | const loadedFonts = new Set();
25 |
26 | // Cache for the Google Fonts API response
27 | let fontsCache: GoogleFont[] | null = null;
28 | let fontsCacheTimestamp: number | null = null;
29 | const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
30 |
31 | export async function fetchGoogleFonts(): Promise {
32 | // Check if we have a valid cache
33 | if (
34 | fontsCache &&
35 | fontsCacheTimestamp &&
36 | Date.now() - fontsCacheTimestamp < CACHE_DURATION
37 | ) {
38 | return fontsCache;
39 | }
40 |
41 | if (!API_KEY) {
42 | throw new Error("Google Fonts API key is not configured");
43 | }
44 |
45 | try {
46 | const response = await fetch(`${API_URL}?key=${API_KEY}&sort=popularity`);
47 | if (!response.ok) {
48 | throw new Error("Failed to fetch Google Fonts");
49 | }
50 | const data = await response.json();
51 | fontsCache = data.items;
52 | fontsCacheTimestamp = Date.now();
53 | return data.items;
54 | } catch (error) {
55 | // If fetch fails and we have a cache, return it even if expired
56 | if (fontsCache) {
57 | return fontsCache;
58 | }
59 | console.error("Error fetching Google Fonts:", error);
60 | throw error;
61 | }
62 | }
63 |
64 | export function getFontUrl(font: GoogleFont, variant = "regular"): string {
65 | const fontFamily = font.family.replace(/\s+/g, "+");
66 | const fontVariant = variant === "regular" ? "400" : variant;
67 | return `https://fonts.googleapis.com/css2?family=${fontFamily}:wght@${fontVariant}&display=swap`;
68 | }
69 |
70 | export async function loadFont(
71 | fontFamily: string,
72 | variant = "regular"
73 | ): Promise {
74 | if (loadedFonts.has(fontFamily)) {
75 | return;
76 | }
77 |
78 | return new Promise((resolve, reject) => {
79 | const link = document.createElement("link");
80 | link.href = getFontUrl({ family: fontFamily } as GoogleFont, variant);
81 | link.rel = "stylesheet";
82 |
83 | link.onload = () => {
84 | loadedFonts.add(fontFamily);
85 | resolve();
86 | };
87 |
88 | link.onerror = () => {
89 | reject(new Error(`Failed to load font: ${fontFamily}`));
90 | };
91 |
92 | document.head.appendChild(link);
93 | });
94 | }
95 |
96 | export interface FontPickerProps {
97 | onFontSelect?: (font: GoogleFont) => void;
98 | value?: string;
99 | }
100 |
101 | export const FONT_CATEGORIES = [
102 | "serif",
103 | "sans-serif",
104 | "display",
105 | "handwriting",
106 | "monospace",
107 | ] as const;
108 |
109 | export type FontCategory = (typeof FONT_CATEGORIES)[number];
110 |
111 | export const FONT_WEIGHTS = [
112 | "100",
113 | "200",
114 | "300",
115 | "400",
116 | "500",
117 | "600",
118 | "700",
119 | "800",
120 | "900",
121 | ] as const;
122 |
123 | export type FontWeight = (typeof FONT_WEIGHTS)[number];
124 | ```
125 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | )
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps) {
54 | return (
55 |
56 |
57 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
82 | )
83 | }
84 |
85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86 | return (
87 |
95 | )
96 | }
97 |
98 | function DialogTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | )
109 | }
110 |
111 | function DialogDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | )
122 | }
123 |
124 | export {
125 | Dialog,
126 | DialogClose,
127 | DialogContent,
128 | DialogDescription,
129 | DialogFooter,
130 | DialogHeader,
131 | DialogOverlay,
132 | DialogPortal,
133 | DialogTitle,
134 | DialogTrigger,
135 | }
136 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form"
15 |
16 | import { cn } from "@/lib/utils"
17 | import { Label } from "@/components/ui/label"
18 |
19 | const Form = FormProvider
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName
26 | }
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue
30 | )
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext)
47 | const itemContext = React.useContext(FormItemContext)
48 | const { getFieldState } = useFormContext()
49 | const formState = useFormState({ name: fieldContext.name })
50 | const fieldState = getFieldState(fieldContext.name, formState)
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ")
54 | }
55 |
56 | const { id } = itemContext
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | }
66 | }
67 |
68 | type FormItemContextValue = {
69 | id: string
70 | }
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue
74 | )
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
86 |
87 | )
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField()
95 |
96 | return (
97 |
104 | )
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | }
124 |
125 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126 | const { formDescriptionId } = useFormField()
127 |
128 | return (
129 |
135 | )
136 | }
137 |
138 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139 | const { error, formMessageId } = useFormField()
140 | const body = error ? String(error?.message ?? "") : props.children
141 |
142 | if (!body) {
143 | return null
144 | }
145 |
146 | return (
147 |
153 | {body}
154 |
155 | )
156 | }
157 |
158 | export {
159 | useFormField,
160 | Form,
161 | FormItem,
162 | FormLabel,
163 | FormControl,
164 | FormDescription,
165 | FormMessage,
166 | FormField,
167 | }
168 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | @theme inline {
7 | --color-background: var(--background);
8 | --color-foreground: var(--foreground);
9 | --font-sans: var(--font-geist-sans);
10 | --font-mono: var(--font-geist-mono);
11 | --color-sidebar-ring: var(--sidebar-ring);
12 | --color-sidebar-border: var(--sidebar-border);
13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14 | --color-sidebar-accent: var(--sidebar-accent);
15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16 | --color-sidebar-primary: var(--sidebar-primary);
17 | --color-sidebar-foreground: var(--sidebar-foreground);
18 | --color-sidebar: var(--sidebar);
19 | --color-chart-5: var(--chart-5);
20 | --color-chart-4: var(--chart-4);
21 | --color-chart-3: var(--chart-3);
22 | --color-chart-2: var(--chart-2);
23 | --color-chart-1: var(--chart-1);
24 | --color-ring: var(--ring);
25 | --color-input: var(--input);
26 | --color-border: var(--border);
27 | --color-destructive: var(--destructive);
28 | --color-accent-foreground: var(--accent-foreground);
29 | --color-accent: var(--accent);
30 | --color-muted-foreground: var(--muted-foreground);
31 | --color-muted: var(--muted);
32 | --color-secondary-foreground: var(--secondary-foreground);
33 | --color-secondary: var(--secondary);
34 | --color-primary-foreground: var(--primary-foreground);
35 | --color-primary: var(--primary);
36 | --color-popover-foreground: var(--popover-foreground);
37 | --color-popover: var(--popover);
38 | --color-card-foreground: var(--card-foreground);
39 | --color-card: var(--card);
40 | --radius-sm: calc(var(--radius) - 4px);
41 | --radius-md: calc(var(--radius) - 2px);
42 | --radius-lg: var(--radius);
43 | --radius-xl: calc(var(--radius) + 4px);
44 | }
45 |
46 | :root {
47 | --radius: 0.625rem;
48 | --background: oklch(1 0 0);
49 | --foreground: oklch(0.145 0 0);
50 | --card: oklch(1 0 0);
51 | --card-foreground: oklch(0.145 0 0);
52 | --popover: oklch(1 0 0);
53 | --popover-foreground: oklch(0.145 0 0);
54 | --primary: oklch(0.205 0 0);
55 | --primary-foreground: oklch(0.985 0 0);
56 | --secondary: oklch(0.97 0 0);
57 | --secondary-foreground: oklch(0.205 0 0);
58 | --muted: oklch(0.97 0 0);
59 | --muted-foreground: oklch(0.556 0 0);
60 | --accent: oklch(0.97 0 0);
61 | --accent-foreground: oklch(0.205 0 0);
62 | --destructive: oklch(0.577 0.245 27.325);
63 | --border: oklch(0.922 0 0);
64 | --input: oklch(0.922 0 0);
65 | --ring: oklch(0.708 0 0);
66 | --chart-1: oklch(0.646 0.222 41.116);
67 | --chart-2: oklch(0.6 0.118 184.704);
68 | --chart-3: oklch(0.398 0.07 227.392);
69 | --chart-4: oklch(0.828 0.189 84.429);
70 | --chart-5: oklch(0.769 0.188 70.08);
71 | --sidebar: oklch(0.985 0 0);
72 | --sidebar-foreground: oklch(0.145 0 0);
73 | --sidebar-primary: oklch(0.205 0 0);
74 | --sidebar-primary-foreground: oklch(0.985 0 0);
75 | --sidebar-accent: oklch(0.97 0 0);
76 | --sidebar-accent-foreground: oklch(0.205 0 0);
77 | --sidebar-border: oklch(0.922 0 0);
78 | --sidebar-ring: oklch(0.708 0 0);
79 | }
80 |
81 | .dark {
82 | --background: oklch(0.145 0 0);
83 | --foreground: oklch(0.985 0 0);
84 | --card: oklch(0.205 0 0);
85 | --card-foreground: oklch(0.985 0 0);
86 | --popover: oklch(0.205 0 0);
87 | --popover-foreground: oklch(0.985 0 0);
88 | --primary: oklch(0.922 0 0);
89 | --primary-foreground: oklch(0.205 0 0);
90 | --secondary: oklch(0.269 0 0);
91 | --secondary-foreground: oklch(0.985 0 0);
92 | --muted: oklch(0.269 0 0);
93 | --muted-foreground: oklch(0.708 0 0);
94 | --accent: oklch(0.269 0 0);
95 | --accent-foreground: oklch(0.985 0 0);
96 | --destructive: oklch(0.704 0.191 22.216);
97 | --border: oklch(1 0 0 / 10%);
98 | --input: oklch(1 0 0 / 15%);
99 | --ring: oklch(0.556 0 0);
100 | --chart-1: oklch(0.488 0.243 264.376);
101 | --chart-2: oklch(0.696 0.17 162.48);
102 | --chart-3: oklch(0.769 0.188 70.08);
103 | --chart-4: oklch(0.627 0.265 303.9);
104 | --chart-5: oklch(0.645 0.246 16.439);
105 | --sidebar: oklch(0.205 0 0);
106 | --sidebar-foreground: oklch(0.985 0 0);
107 | --sidebar-primary: oklch(0.488 0.243 264.376);
108 | --sidebar-primary-foreground: oklch(0.985 0 0);
109 | --sidebar-accent: oklch(0.269 0 0);
110 | --sidebar-accent-foreground: oklch(0.985 0 0);
111 | --sidebar-border: oklch(1 0 0 / 10%);
112 | --sidebar-ring: oklch(0.556 0 0);
113 | }
114 |
115 | @layer base {
116 | * {
117 | @apply border-border outline-ring/50;
118 | }
119 | body {
120 | @apply bg-background text-foreground;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Command as CommandPrimitive } from "cmdk"
5 | import { SearchIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | } from "@/components/ui/dialog"
15 |
16 | function Command({
17 | className,
18 | ...props
19 | }: React.ComponentProps) {
20 | return (
21 |
29 | )
30 | }
31 |
32 | function CommandDialog({
33 | title = "Command Palette",
34 | description = "Search for a command to run...",
35 | children,
36 | ...props
37 | }: React.ComponentProps & {
38 | title?: string
39 | description?: string
40 | }) {
41 | return (
42 |
53 | )
54 | }
55 |
56 | function CommandInput({
57 | className,
58 | ...props
59 | }: React.ComponentProps) {
60 | return (
61 |
65 |
66 |
74 |
75 | )
76 | }
77 |
78 | function CommandList({
79 | className,
80 | ...props
81 | }: React.ComponentProps) {
82 | return (
83 |
91 | )
92 | }
93 |
94 | function CommandEmpty({
95 | ...props
96 | }: React.ComponentProps) {
97 | return (
98 |
103 | )
104 | }
105 |
106 | function CommandGroup({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
119 | )
120 | }
121 |
122 | function CommandSeparator({
123 | className,
124 | ...props
125 | }: React.ComponentProps) {
126 | return (
127 |
132 | )
133 | }
134 |
135 | function CommandItem({
136 | className,
137 | ...props
138 | }: React.ComponentProps) {
139 | return (
140 |
148 | )
149 | }
150 |
151 | function CommandShortcut({
152 | className,
153 | ...props
154 | }: React.ComponentProps<"span">) {
155 | return (
156 |
164 | )
165 | }
166 |
167 | export {
168 | Command,
169 | CommandDialog,
170 | CommandInput,
171 | CommandList,
172 | CommandEmpty,
173 | CommandGroup,
174 | CommandItem,
175 | CommandShortcut,
176 | CommandSeparator,
177 | }
178 |
--------------------------------------------------------------------------------
/app/(home)/sections/setup.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { allSnippets, Snippet as SnippetType } from "contentlayer/generated";
3 |
4 |
5 |
6 | import CodeBlock from "@/components/code-block";
7 | import { Snippet } from "@/components/snippet";
8 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
9 |
10 | const snippets: SnippetType[] = allSnippets.sort((a, b) => a.order - b.order);
11 |
12 | export default function Setup() {
13 | return (
14 |
15 |
16 | Setup
17 |
18 |
19 |
20 | Install Shadcn via CLI
21 |
22 |
23 | Run the{" "}
24 |
25 | shadcn
26 | {" "}
27 | init command to setup your project:
28 |
29 |
30 |
31 |
32 |
33 | Install necessary Shadcn components:
34 |
35 |
36 | Run the{" "}
37 |
38 | shadcn
39 | {" "}
40 | add command to add the necessary shadcn components to your project:
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 | Install necessary React packages:
52 |
53 |
54 |
55 |
56 |
57 |
58 | Generate a Google Fonts API key:
59 |
60 |
61 | Go to the{" "}
62 |
68 | Google Fonts API console
69 |
70 |
71 |
72 |
73 |
74 | Update the{" "}
75 |
76 | .env
77 | {" "}
78 | file:
79 |
80 |
84 |
85 |
86 |
87 | To use the font picker component:
88 |
89 |
90 | Import the font picker component:
91 |
92 |
96 |
97 | Use the font picker component:
98 |
99 |
setFont(font)} value={font} />`}
101 | className="mt-2"
102 | />
103 |
104 | Copy the code from the snippet below and paste it in your component
105 | file.
106 |
107 |
108 |
109 | Snippets
110 |
111 |
112 | {snippets.map((snippet) => (
113 |
114 |
115 |
116 | {snippet.file}{" "}
117 |
118 | ({snippet.filePath})
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | ))}
127 |
128 |
129 |
130 |
131 | );
132 | }
--------------------------------------------------------------------------------
/app/(home)/sections/hero.tsx:
--------------------------------------------------------------------------------
1 | import CodeBlock from "@/components/code-block";
2 | // import { Badge } from "@/components/ui/badge";
3 | import { Button, buttonVariants } from "@/components/ui/button";
4 | import { Card, CardContent } from "@/components/ui/card";
5 | import { FontPicker } from "@/components/ui/font-picker";
6 | import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
7 | import { siteConfig } from "@/config/site";
8 | import startuplistLogo from "@/public/startup-listing.svg";
9 | import { zodResolver } from "@hookform/resolvers/zod";
10 | import { ArrowRight } from "lucide-react";
11 | import Image from "next/image";
12 | import Link from "next/link";
13 | import { useForm } from "react-hook-form";
14 | import { toast } from "sonner";
15 | import { z } from "zod";
16 |
17 |
18 | const FormSchema = z.object({
19 | font: z.string().min(1, {
20 | message: "Font name is required",
21 | }),
22 | });
23 |
24 | export default function Hero() {
25 | const form = useForm>({
26 | resolver: zodResolver(FormSchema),
27 | defaultValues: {
28 | font: "",
29 | },
30 | });
31 |
32 | function onSubmit(data: z.infer) {
33 | toast.success("You submitted the following values:", {
34 | description: (
35 |
36 |
37 | {JSON.stringify(data, null, 2)}
38 |
39 |
40 | ),
41 | });
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 |
52 |
53 |
54 |
55 |
61 | `${src}?w=${width}&q=${quality}`
62 | }
63 | priority
64 | className="float-left flex flex-shrink-0 rounded-sm align-middle mix-blend-multiply mr-1.5 dark:mix-blend-normal dark:invert"
65 | />
66 | {/* Startup Listing */}
67 |
68 | - Discover the best startups, products and projects.
69 |
70 |
71 |
78 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | Shadcn Font Picker
92 |
93 |
94 | An implementation of a Font Picker component for React, built
95 | on top of Shadcn UI's input component and Google Fonts API.
96 |
97 |
98 |
105 | Try it out
106 |
107 |
114 | Github
115 |
116 |
117 |
121 |
122 |
123 |
158 |
159 |
160 | );
161 | }
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default"
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | )
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | )
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | )
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | )
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | )
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | }
186 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import { Check, ChevronRight, Circle } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | );
181 | };
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | };
201 |
--------------------------------------------------------------------------------
/components/ui/font-picker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Command,
6 | CommandEmpty,
7 | CommandGroup,
8 | CommandInput,
9 | CommandItem,
10 | } from "@/components/ui/command";
11 | import {
12 | DropdownMenu,
13 | DropdownMenuContent,
14 | DropdownMenuRadioGroup,
15 | DropdownMenuRadioItem,
16 | DropdownMenuTrigger,
17 | } from "@/components/ui/dropdown-menu";
18 | import {
19 | Popover,
20 | PopoverContent,
21 | PopoverTrigger,
22 | } from "@/components/ui/popover";
23 | import type { GoogleFont } from "@/lib/fonts";
24 | import { fetchGoogleFonts, loadFont } from "@/lib/fonts";
25 | import { cn } from "@/lib/utils";
26 | import { Check, ChevronsUpDown, Filter } from "lucide-react";
27 | import * as React from "react";
28 | import { ComponentType } from "react";
29 | import { FixedSizeList as _FixedSizeList, FixedSizeListProps } from "react-window";
30 |
31 | const FixedSizeList = _FixedSizeList as ComponentType;
32 |
33 | function FontListItem({
34 | font,
35 | isSelected,
36 | onSelect,
37 | }: {
38 | font: GoogleFont;
39 | isSelected: boolean;
40 | onSelect: () => void;
41 | }) {
42 | const [isFontLoaded, setIsFontLoaded] = React.useState(false);
43 |
44 | React.useEffect(() => {
45 | if (!isFontLoaded) {
46 | loadFont(font.family)
47 | .then(() => setIsFontLoaded(true))
48 | .catch((error) => console.error("Failed to load font:", error));
49 | }
50 | }, [isFontLoaded, font.family]);
51 |
52 | return (
53 |
59 |
65 |
66 | {font.family}
67 |
76 | The quick brown fox
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | interface FontPickerProps {
84 | onChange?: (font: GoogleFont["family"]) => void;
85 | value?: string;
86 | width?: number;
87 | height?: number;
88 | className?: string;
89 | showFilters?: boolean;
90 | }
91 |
92 | export function FontPicker({
93 | onChange,
94 | value,
95 | width = 300,
96 | height = 300,
97 | className,
98 | showFilters = true,
99 | }: FontPickerProps) {
100 | const [selectedFont, setSelectedFont] = React.useState(
101 | null,
102 | );
103 | const [search, setSearch] = React.useState("");
104 | const [isOpen, setIsOpen] = React.useState(false);
105 | const [selectedCategory, setSelectedCategory] = React.useState("all");
106 | const [fonts, setFonts] = React.useState([]);
107 | const [isLoading, setIsLoading] = React.useState(true);
108 | const [error, setError] = React.useState(null);
109 | const buttonRef = React.useRef(null);
110 |
111 | React.useEffect(() => {
112 | const loadFonts = async () => {
113 | try {
114 | setIsLoading(true);
115 | const fetchedFonts = await fetchGoogleFonts();
116 | setFonts(fetchedFonts);
117 | const font = fetchedFonts.find((font) => font.family === value);
118 | if (font) {
119 | setSelectedFont(font);
120 | }
121 | setError(null);
122 | } catch (err) {
123 | setError(
124 | err instanceof Error ? err : new Error("Failed to load fonts"),
125 | );
126 | console.error("Error loading fonts:", err);
127 | } finally {
128 | setIsLoading(false);
129 | }
130 | };
131 |
132 | loadFonts();
133 | }, [value]);
134 |
135 | const categories = React.useMemo(() => {
136 | const uniqueCategories = new Set(fonts.map((font) => font.category));
137 | return Array.from(uniqueCategories).sort();
138 | }, [fonts]);
139 |
140 | const filteredFonts = React.useMemo(() => {
141 | return fonts.filter((font: GoogleFont) => {
142 | const matchesSearch = font.family
143 | .toLowerCase()
144 | .includes(search.toLowerCase());
145 | const matchesCategory =
146 | !showFilters ||
147 | selectedCategory === "all" ||
148 | font.category === selectedCategory;
149 | return matchesSearch && matchesCategory;
150 | });
151 | }, [fonts, search, selectedCategory, showFilters]);
152 |
153 | const handleSelectFont = React.useCallback(
154 | (font: GoogleFont) => {
155 | setSelectedFont(font);
156 | onChange?.(font.family);
157 | setIsOpen(false);
158 | },
159 | [onChange],
160 | );
161 |
162 | const handleOpenChange = React.useCallback((open: boolean) => {
163 | setIsOpen(open);
164 | }, []);
165 |
166 | const Row = React.useCallback(
167 | ({ index, style }: { index: number; style: React.CSSProperties }) => {
168 | const font = filteredFonts[index];
169 | return (
170 |
171 | handleSelectFont(font)}
175 | />
176 |
177 | );
178 | },
179 | [filteredFonts, selectedFont, handleSelectFont],
180 | );
181 |
182 | return (
183 |
184 |
185 |
204 |
205 |
206 |
207 |
213 |
214 | {showFilters && (
215 |
216 |
217 |
230 |
231 |
232 |
236 |
237 | All Categories
238 |
239 | {categories.map((category) => (
240 |
245 | {category}
246 |
247 | ))}
248 |
249 |
250 |
251 | )}
252 |
253 | {filteredFonts.length} fonts
254 |
255 |
256 | {isLoading ? (
257 |
260 | ) : error ? (
261 |
262 | Failed to load fonts. Please try again later.
263 |
264 | ) : (
265 | <>
266 | No fonts found.
267 |
268 |
269 |
275 | {Row}
276 |
277 |
278 |
279 | >
280 | )}
281 |
282 |
283 |
284 | );
285 | }
--------------------------------------------------------------------------------
/content/snippets/font-picker.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | file: font-picker.tsx
3 | filePath: components/ui/font-picker.tsx
4 | order: 1
5 | description: Use the font picker component
6 | ---
7 |
8 | ```tsx
9 | "use client";
10 |
11 | import { Button } from "@/components/ui/button";
12 | import {
13 | Command,
14 | CommandEmpty,
15 | CommandGroup,
16 | CommandInput,
17 | CommandItem,
18 | } from "@/components/ui/command";
19 | import {
20 | DropdownMenu,
21 | DropdownMenuContent,
22 | DropdownMenuRadioGroup,
23 | DropdownMenuRadioItem,
24 | DropdownMenuTrigger,
25 | } from "@/components/ui/dropdown-menu";
26 | import {
27 | Popover,
28 | PopoverContent,
29 | PopoverTrigger,
30 | } from "@/components/ui/popover";
31 | import type { GoogleFont } from "@/lib/fonts";
32 | import { fetchGoogleFonts, loadFont } from "@/lib/fonts";
33 | import { cn } from "@/lib/utils";
34 | import { Check, ChevronsUpDown, Filter } from "lucide-react";
35 | import * as React from "react";
36 | import { ComponentType } from "react";
37 | import { FixedSizeList as _FixedSizeList, FixedSizeListProps } from "react-window";
38 |
39 | const FixedSizeList = _FixedSizeList as ComponentType;
40 |
41 | function FontListItem({
42 | font,
43 | isSelected,
44 | onSelect,
45 | }: {
46 | font: GoogleFont;
47 | isSelected: boolean;
48 | onSelect: () => void;
49 | }) {
50 | const [isFontLoaded, setIsFontLoaded] = React.useState(false);
51 |
52 | React.useEffect(() => {
53 | if (!isFontLoaded) {
54 | loadFont(font.family)
55 | .then(() => setIsFontLoaded(true))
56 | .catch((error) => console.error("Failed to load font:", error));
57 | }
58 | }, [isFontLoaded, font.family]);
59 |
60 | return (
61 |
67 |
73 |
74 | {font.family}
75 |
84 | The quick brown fox
85 |
86 |
87 |
88 | );
89 | }
90 |
91 | interface FontPickerProps {
92 | onChange?: (font: GoogleFont["family"]) => void;
93 | value?: string;
94 | width?: number;
95 | height?: number;
96 | className?: string;
97 | showFilters?: boolean;
98 | }
99 |
100 | export function FontPicker({
101 | onChange,
102 | value,
103 | width = 300,
104 | height = 300,
105 | className,
106 | showFilters = true,
107 | }: FontPickerProps) {
108 | const [selectedFont, setSelectedFont] = React.useState(
109 | null,
110 | );
111 | const [search, setSearch] = React.useState("");
112 | const [isOpen, setIsOpen] = React.useState(false);
113 | const [selectedCategory, setSelectedCategory] = React.useState("all");
114 | const [fonts, setFonts] = React.useState([]);
115 | const [isLoading, setIsLoading] = React.useState(true);
116 | const [error, setError] = React.useState(null);
117 | const buttonRef = React.useRef(null);
118 |
119 | React.useEffect(() => {
120 | const loadFonts = async () => {
121 | try {
122 | setIsLoading(true);
123 | const fetchedFonts = await fetchGoogleFonts();
124 | setFonts(fetchedFonts);
125 | const font = fetchedFonts.find((font) => font.family === value);
126 | if (font) {
127 | setSelectedFont(font);
128 | }
129 | setError(null);
130 | } catch (err) {
131 | setError(
132 | err instanceof Error ? err : new Error("Failed to load fonts"),
133 | );
134 | console.error("Error loading fonts:", err);
135 | } finally {
136 | setIsLoading(false);
137 | }
138 | };
139 |
140 | loadFonts();
141 | }, [value]);
142 |
143 | const categories = React.useMemo(() => {
144 | const uniqueCategories = new Set(fonts.map((font) => font.category));
145 | return Array.from(uniqueCategories).sort();
146 | }, [fonts]);
147 |
148 | const filteredFonts = React.useMemo(() => {
149 | return fonts.filter((font: GoogleFont) => {
150 | const matchesSearch = font.family
151 | .toLowerCase()
152 | .includes(search.toLowerCase());
153 | const matchesCategory =
154 | !showFilters ||
155 | selectedCategory === "all" ||
156 | font.category === selectedCategory;
157 | return matchesSearch && matchesCategory;
158 | });
159 | }, [fonts, search, selectedCategory, showFilters]);
160 |
161 | const handleSelectFont = React.useCallback(
162 | (font: GoogleFont) => {
163 | setSelectedFont(font);
164 | onChange?.(font.family);
165 | setIsOpen(false);
166 | },
167 | [onChange],
168 | );
169 |
170 | const handleOpenChange = React.useCallback((open: boolean) => {
171 | setIsOpen(open);
172 | }, []);
173 |
174 | const Row = React.useCallback(
175 | ({ index, style }: { index: number; style: React.CSSProperties }) => {
176 | const font = filteredFonts[index];
177 | return (
178 |
179 | handleSelectFont(font)}
183 | />
184 |
185 | );
186 | },
187 | [filteredFonts, selectedFont, handleSelectFont],
188 | );
189 |
190 | return (
191 |
192 |
193 |
212 |
213 |
214 |
215 |
221 |
222 | {showFilters && (
223 |
224 |
225 |
238 |
239 |
240 |
244 |
245 | All Categories
246 |
247 | {categories.map((category) => (
248 |
253 | {category}
254 |
255 | ))}
256 |
257 |
258 |
259 | )}
260 |
261 | {filteredFonts.length} fonts
262 |
263 |
264 | {isLoading ? (
265 |
268 | ) : error ? (
269 |
270 | Failed to load fonts. Please try again later.
271 |
272 | ) : (
273 | <>
274 | No fonts found.
275 |
276 |
277 |
283 | {Row}
284 |
285 |
286 |
287 | >
288 | )}
289 |
290 |
291 |
292 | );
293 | }
294 | ```
--------------------------------------------------------------------------------
/public/r/font-picker.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "font-picker",
4 | "type": "registry:block",
5 | "title": "Font Picker",
6 | "description": "A font picker component.",
7 | "dependencies": [
8 | "react-window",
9 | "@types/react-window"
10 | ],
11 | "registryDependencies": [
12 | "button",
13 | "command",
14 | "dropdown-menu",
15 | "popover"
16 | ],
17 | "files": [
18 | {
19 | "path": "components/ui/font-picker.tsx",
20 | "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n} from \"@/components/ui/command\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport type { GoogleFont } from \"@/lib/fonts\";\nimport { fetchGoogleFonts, loadFont } from \"@/lib/fonts\";\nimport { cn } from \"@/lib/utils\";\nimport { Check, ChevronsUpDown, Filter } from \"lucide-react\";\nimport * as React from \"react\";\nimport { ComponentType } from \"react\";\nimport { FixedSizeList as _FixedSizeList, FixedSizeListProps } from \"react-window\";\n\nconst FixedSizeList = _FixedSizeList as ComponentType;\n\nfunction FontListItem({\n font,\n isSelected,\n onSelect,\n}: {\n font: GoogleFont;\n isSelected: boolean;\n onSelect: () => void;\n}) {\n const [isFontLoaded, setIsFontLoaded] = React.useState(false);\n\n React.useEffect(() => {\n if (!isFontLoaded) {\n loadFont(font.family)\n .then(() => setIsFontLoaded(true))\n .catch((error) => console.error(\"Failed to load font:\", error));\n }\n }, [isFontLoaded, font.family]);\n\n return (\n \n \n \n {font.family}\n \n The quick brown fox\n \n
\n \n );\n}\n\ninterface FontPickerProps {\n onChange?: (font: GoogleFont[\"family\"]) => void;\n value?: string;\n width?: number;\n height?: number;\n className?: string;\n showFilters?: boolean;\n}\n\nexport function FontPicker({\n onChange,\n value,\n width = 300,\n height = 300,\n className,\n showFilters = true,\n}: FontPickerProps) {\n const [selectedFont, setSelectedFont] = React.useState(\n null,\n );\n const [search, setSearch] = React.useState(\"\");\n const [isOpen, setIsOpen] = React.useState(false);\n const [selectedCategory, setSelectedCategory] = React.useState(\"all\");\n const [fonts, setFonts] = React.useState([]);\n const [isLoading, setIsLoading] = React.useState(true);\n const [error, setError] = React.useState(null);\n const buttonRef = React.useRef(null);\n\n React.useEffect(() => {\n const loadFonts = async () => {\n try {\n setIsLoading(true);\n const fetchedFonts = await fetchGoogleFonts();\n setFonts(fetchedFonts);\n const font = fetchedFonts.find((font) => font.family === value);\n if (font) {\n setSelectedFont(font);\n }\n setError(null);\n } catch (err) {\n setError(\n err instanceof Error ? err : new Error(\"Failed to load fonts\"),\n );\n console.error(\"Error loading fonts:\", err);\n } finally {\n setIsLoading(false);\n }\n };\n\n loadFonts();\n }, [value]);\n\n const categories = React.useMemo(() => {\n const uniqueCategories = new Set(fonts.map((font) => font.category));\n return Array.from(uniqueCategories).sort();\n }, [fonts]);\n\n const filteredFonts = React.useMemo(() => {\n return fonts.filter((font: GoogleFont) => {\n const matchesSearch = font.family\n .toLowerCase()\n .includes(search.toLowerCase());\n const matchesCategory =\n !showFilters ||\n selectedCategory === \"all\" ||\n font.category === selectedCategory;\n return matchesSearch && matchesCategory;\n });\n }, [fonts, search, selectedCategory, showFilters]);\n\n const handleSelectFont = React.useCallback(\n (font: GoogleFont) => {\n setSelectedFont(font);\n onChange?.(font.family);\n setIsOpen(false);\n },\n [onChange],\n );\n\n const handleOpenChange = React.useCallback((open: boolean) => {\n setIsOpen(open);\n }, []);\n\n const Row = React.useCallback(\n ({ index, style }: { index: number; style: React.CSSProperties }) => {\n const font = filteredFonts[index];\n return (\n \n handleSelectFont(font)}\n />\n
\n );\n },\n [filteredFonts, selectedFont, handleSelectFont],\n );\n\n return (\n \n \n \n \n \n \n \n \n {showFilters && (\n \n \n \n \n \n \n \n All Categories\n \n {categories.map((category) => (\n \n {category}\n \n ))}\n \n \n \n )}\n \n {filteredFonts.length} fonts\n \n
\n {isLoading ? (\n \n ) : error ? (\n \n Failed to load fonts. Please try again later.\n
\n ) : (\n <>\n No fonts found.\n \n \n \n {Row}\n \n
\n \n >\n )}\n \n \n \n );\n}",
21 | "type": "registry:component"
22 | },
23 | {
24 | "path": "lib/fonts.ts",
25 | "content": "export interface GoogleFont {\n family: string;\n variants: string[];\n subsets: string[];\n version: string;\n lastModified: string;\n files: Record;\n category: string;\n kind: string;\n}\n\nconst API_KEY = process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY;\nconst API_URL = \"https://www.googleapis.com/webfonts/v1/webfonts\";\n\n// Cache for loaded font stylesheets\nconst loadedFonts = new Set();\n\n// Cache for the Google Fonts API response\nlet fontsCache: GoogleFont[] | null = null;\nlet fontsCacheTimestamp: number | null = null;\nconst CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours\n\nexport async function fetchGoogleFonts(): Promise {\n // Check if we have a valid cache\n if (\n fontsCache &&\n fontsCacheTimestamp &&\n Date.now() - fontsCacheTimestamp < CACHE_DURATION\n ) {\n return fontsCache;\n }\n\n if (!API_KEY) {\n throw new Error(\"Google Fonts API key is not configured\");\n }\n\n try {\n const response = await fetch(`${API_URL}?key=${API_KEY}&sort=popularity`);\n if (!response.ok) {\n throw new Error(\"Failed to fetch Google Fonts\");\n }\n const data = await response.json();\n fontsCache = data.items;\n fontsCacheTimestamp = Date.now();\n return data.items;\n } catch (error) {\n // If fetch fails and we have a cache, return it even if expired\n if (fontsCache) {\n return fontsCache;\n }\n console.error(\"Error fetching Google Fonts:\", error);\n throw error;\n }\n}\n\nexport function getFontUrl(font: GoogleFont, variant = \"regular\"): string {\n const fontFamily = font.family.replace(/\\s+/g, \"+\");\n const fontVariant = variant === \"regular\" ? \"400\" : variant;\n return `https://fonts.googleapis.com/css2?family=${fontFamily}:wght@${fontVariant}&display=swap`;\n}\n\nexport async function loadFont(\n fontFamily: string,\n variant = \"regular\",\n): Promise {\n if (loadedFonts.has(fontFamily)) {\n return;\n }\n\n return new Promise((resolve, reject) => {\n const link = document.createElement(\"link\");\n link.href = getFontUrl({ family: fontFamily } as GoogleFont, variant);\n link.rel = \"stylesheet\";\n\n link.onload = () => {\n loadedFonts.add(fontFamily);\n resolve();\n };\n\n link.onerror = () => {\n reject(new Error(`Failed to load font: ${fontFamily}`));\n };\n\n document.head.appendChild(link);\n });\n}\n\nexport interface FontPickerProps {\n onFontSelect?: (font: GoogleFont) => void;\n value?: string;\n}\n\nexport const FONT_CATEGORIES = [\n \"serif\",\n \"sans-serif\",\n \"display\",\n \"handwriting\",\n \"monospace\",\n] as const;\n\nexport type FontCategory = (typeof FONT_CATEGORIES)[number];\n\nexport const FONT_WEIGHTS = [\n \"100\",\n \"200\",\n \"300\",\n \"400\",\n \"500\",\n \"600\",\n \"700\",\n \"800\",\n \"900\",\n] as const;\n\nexport type FontWeight = (typeof FONT_WEIGHTS)[number];\n",
26 | "type": "registry:lib"
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/public/startup-listing.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------