├── bun.lockb
├── app
├── favicon.ico
├── twitter-image.png
├── opengraph-image.png
├── layout.tsx
├── registry
│ └── [name]
│ │ └── route.ts
├── page.mdx
└── globals.css
├── .prettierrc
├── postcss.config.mjs
├── hooks
├── use-mounted.ts
└── config.ts
├── registry
├── example
│ ├── metal-button-gold.tsx
│ ├── metal-button-demo.tsx
│ ├── metal-button-error.tsx
│ ├── metal-button-bronze.tsx
│ ├── metal-button-primary.tsx
│ └── metal-button-success.tsx
└── metal-button
│ └── metal-button.tsx
├── components
├── ui
│ ├── collapsible.tsx
│ ├── accordion.tsx
│ ├── tabs.tsx
│ └── button.tsx
├── github-btn.tsx
├── component-source.tsx
├── component-wrapper.tsx
├── code-block-with-copy.tsx
├── blur-bottom.tsx
├── open-in-v0.tsx
├── code-block-wrapper.tsx
├── code-block-command.tsx
├── copy-button.tsx
├── metal-button.tsx
├── dropdown-menu.tsx
└── component-preview.tsx
├── eslint.config.mjs
├── components.json
├── .gitignore
├── lib
├── utils.ts
└── metal-source.mdx
├── types
└── unist.ts
├── tsconfig.json
├── next.config.mjs
├── public
└── r
│ ├── metal-button-gold.json
│ ├── metal-button-demo.json
│ ├── metal-button-error.json
│ ├── metal-button-bronze.json
│ ├── metal-button-primary.json
│ ├── metal-button-success.json
│ └── metal-button.json
├── LICENSE
├── sync-mdx.ts
├── package.json
├── README.md
├── registry.json
└── mdx-components.tsx
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lakshaybhushan/metal-buttons/HEAD/bun.lockb
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lakshaybhushan/metal-buttons/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lakshaybhushan/metal-buttons/HEAD/app/twitter-image.png
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lakshaybhushan/metal-buttons/HEAD/app/opengraph-image.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "bracketSpacing": true,
4 | "plugins": ["prettier-plugin-tailwindcss"]
5 | }
6 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | setMounted(true);
8 | }, []);
9 |
10 | return mounted;
11 | }
--------------------------------------------------------------------------------
/registry/example/metal-button-gold.tsx:
--------------------------------------------------------------------------------
1 | import { MetalButton } from "@/components/metal-button";
2 |
3 | export function MetalButtonGold() {
4 | return (
5 |
6 | Gold
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/registry/example/metal-button-demo.tsx:
--------------------------------------------------------------------------------
1 | import { MetalButton } from "@/components/metal-button";
2 |
3 | export function MetalButtonDemo() {
4 | return (
5 |
6 | Button
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/registry/example/metal-button-error.tsx:
--------------------------------------------------------------------------------
1 | import { MetalButton } from "@/components/metal-button";
2 |
3 | export function MetalButtonError() {
4 | return (
5 |
6 | Error
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/registry/example/metal-button-bronze.tsx:
--------------------------------------------------------------------------------
1 | import { MetalButton } from "@/components/metal-button";
2 |
3 | export function MetalButtonBronze() {
4 | return (
5 |
6 | Bronze
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/registry/example/metal-button-primary.tsx:
--------------------------------------------------------------------------------
1 | import { MetalButton } from "@/components/metal-button";
2 |
3 | export function MetalButtonPrimary() {
4 | return (
5 |
6 | Primary
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/registry/example/metal-button-success.tsx:
--------------------------------------------------------------------------------
1 | import { MetalButton } from "@/components/metal-button";
2 |
3 | export function MetalButtonSuccess() {
4 | return (
5 |
6 | Success
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/hooks/config.ts:
--------------------------------------------------------------------------------
1 | import { useAtom } from "jotai";
2 | import { atomWithStorage } from "jotai/utils";
3 |
4 | type Config = {
5 | packageManager: "npm" | "yarn" | "pnpm" | "bun";
6 | };
7 |
8 | const configAtom = atomWithStorage("config", {
9 | packageManager: "pnpm",
10 | });
11 |
12 | export function useConfig() {
13 | return useAtom(configAtom);
14 | }
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4 |
5 | const Collapsible = CollapsiblePrimitive.Root;
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent };
12 |
--------------------------------------------------------------------------------
/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.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 | }
--------------------------------------------------------------------------------
/components/github-btn.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Button } from "./ui/button";
3 | import { FaGithub } from "react-icons/fa";
4 |
5 | export function GithubBtn() {
6 | return (
7 |
8 |
12 |
13 |
14 | GitHub
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/component-source.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { CodeBlockWrapper } from "@/components/code-block-wrapper";
7 |
8 | interface ComponentSourceProps extends React.HTMLAttributes {
9 | src: string;
10 | }
11 |
12 | export function ComponentSource({
13 | children,
14 | className,
15 | ...props
16 | }: ComponentSourceProps) {
17 | return (
18 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/.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.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | .content-collections
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | const SITE = "https://button.lakshb.dev";
9 | const REGISTRY_JSON = "metal-button.json";
10 | const EXAMPLE_JSON = "metal-button-demo.json";
11 | export const URL = `${SITE}/r/${REGISTRY_JSON}`;
12 | export const EXAMPLE_URL = `${SITE}/r/${EXAMPLE_JSON}`;
13 |
14 | export const npmCommand = `npx shadcn@latest add "${URL}"`;
15 | export const yarnCommand = `npx shadcn@latest add "${URL}"`;
16 | export const pnpmCommand = `pnpm dlx shadcn@latest add "${URL}"`;
17 | export const bunCommand = `bunx --bun shadcn@latest add "${URL}"`;
--------------------------------------------------------------------------------
/types/unist.ts:
--------------------------------------------------------------------------------
1 | import type { Node } from "unist";
2 | export interface UnistNode extends Node {
3 | type: string;
4 | name?: string;
5 | tagName?: string;
6 | value?: string;
7 | properties?: {
8 | __rawString__?: string;
9 | __className__?: string;
10 | __event__?: string;
11 | [key: string]: unknown;
12 | } & NpmCommands;
13 | attributes?: {
14 | name: string;
15 | value: unknown;
16 | type?: string;
17 | }[];
18 | children?: UnistNode[];
19 | }
20 |
21 | export interface UnistTree extends UnistNode {
22 | children: UnistNode[];
23 | }
24 |
25 | export interface NpmCommands {
26 | __npmCommand__?: string;
27 | __yarnCommand__?: string;
28 | __pnpmCommand__?: string;
29 | __bunCommand__?: string;
30 | }
31 |
--------------------------------------------------------------------------------
/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 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts",
30 | "app/page.mdx"
31 | , "next.config.mjs" ],
32 | "exclude": ["node_modules"]
33 | }
34 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | /** @type {import('rehype-pretty-code').Options} */
3 |
4 | import createMDX from "@next/mdx";
5 | import rehypeSlug from "rehype-slug";
6 | import rehypePrettyCode from "rehype-pretty-code";
7 | import remarkGfm from "remark-gfm";
8 | import rehypeAutolinkHeadings from "rehype-autolink-headings";
9 |
10 | const options = {
11 | theme: "poimandres",
12 | };
13 |
14 | const withMDX = createMDX({
15 | extension: /\.mdx?$/,
16 | options: {
17 | remarkPlugins: [remarkGfm],
18 | rehypePlugins: [
19 | [rehypePrettyCode, options],
20 | [rehypeAutolinkHeadings],
21 | rehypeSlug,
22 | ],
23 | },
24 | });
25 |
26 | const nextConfig = {
27 | eslint: {
28 | ignoreDuringBuilds: true,
29 | },
30 | pageExtensions: ["mdx", "ts", "tsx"],
31 | };
32 | export default withMDX(nextConfig);
33 |
--------------------------------------------------------------------------------
/public/r/metal-button-gold.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button-gold",
4 | "type": "registry:ui",
5 | "title": "Metal Button Gold",
6 | "author": "Lakshay Bhushan",
7 | "description": "A shadcn/ui based button but it's made of gold metal",
8 | "registryDependencies": [
9 | "https://button.lakshb.dev/r/metal-button.json"
10 | ],
11 | "files": [
12 | {
13 | "path": "registry/example/metal-button-gold.tsx",
14 | "content": "import { MetalButton } from \"@/components/metal-button\";\n\nexport function MetalButtonGold() {\n return (\n \n Gold \n
\n );\n}\n",
15 | "type": "registry:ui",
16 | "target": "components/metal-button-gold.tsx"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/public/r/metal-button-demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button-demo",
4 | "type": "registry:page",
5 | "title": "Metal Button Basic Example",
6 | "author": "Lakshay Bhushan",
7 | "description": "A basic example of the metal button component",
8 | "registryDependencies": [
9 | "https://button.lakshb.dev/r/metal-button.json"
10 | ],
11 | "files": [
12 | {
13 | "path": "registry/example/metal-button-demo.tsx",
14 | "content": "import { MetalButton } from \"@/components/metal-button\";\n\nexport function MetalButtonDemo() {\n return (\n \n Button \n
\n );\n}\n",
15 | "type": "registry:page",
16 | "target": "components/metal-button-demo.tsx"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/public/r/metal-button-error.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button-error",
4 | "type": "registry:ui",
5 | "title": "Metal Button Error",
6 | "author": "Lakshay Bhushan",
7 | "description": "A shadcn/ui based button but it's made of metal and it's red",
8 | "registryDependencies": [
9 | "https://button.lakshb.dev/r/metal-button.json"
10 | ],
11 | "files": [
12 | {
13 | "path": "registry/example/metal-button-error.tsx",
14 | "content": "import { MetalButton } from \"@/components/metal-button\";\n\nexport function MetalButtonError() {\n return (\n \n Error \n
\n );\n}\n",
15 | "type": "registry:ui",
16 | "target": "components/metal-button-error.tsx"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/public/r/metal-button-bronze.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button-bronze",
4 | "type": "registry:ui",
5 | "title": "Metal Button Bronze",
6 | "author": "Lakshay Bhushan",
7 | "description": "A shadcn/ui based button but it's made of bronze metal",
8 | "registryDependencies": [
9 | "https://button.lakshb.dev/r/metal-button.json"
10 | ],
11 | "files": [
12 | {
13 | "path": "registry/example/metal-button-bronze.tsx",
14 | "content": "import { MetalButton } from \"@/components/metal-button\";\n\nexport function MetalButtonBronze() {\n return (\n \n Bronze \n
\n );\n}\n",
15 | "type": "registry:ui",
16 | "target": "components/metal-button-bronze.tsx"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/public/r/metal-button-primary.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button-primary",
4 | "type": "registry:ui",
5 | "title": "Metal Button Primary",
6 | "author": "Lakshay Bhushan",
7 | "description": "A shadcn/ui based button but it's made of metal and it's blue",
8 | "registryDependencies": [
9 | "https://button.lakshb.dev/r/metal-button.json"
10 | ],
11 | "files": [
12 | {
13 | "path": "registry/example/metal-button-primary.tsx",
14 | "content": "import { MetalButton } from \"@/components/metal-button\";\n\nexport function MetalButtonPrimary() {\n return (\n \n Primary \n
\n );\n}\n",
15 | "type": "registry:ui",
16 | "target": "components/metal-button-primary.tsx"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/public/r/metal-button-success.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button-success",
4 | "type": "registry:ui",
5 | "title": "Metal Button Success",
6 | "author": "Lakshay Bhushan",
7 | "description": "A shadcn/ui based button but it's made of metal and it's green",
8 | "registryDependencies": [
9 | "https://button.lakshb.dev/r/metal-button.json"
10 | ],
11 | "files": [
12 | {
13 | "path": "registry/example/metal-button-success.tsx",
14 | "content": "import { MetalButton } from \"@/components/metal-button\";\n\nexport function MetalButtonSuccess() {\n return (\n \n Success \n
\n );\n}\n",
15 | "type": "registry:ui",
16 | "target": "components/metal-button-success.tsx"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/components/component-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { OpenInV0Button } from "@/components/open-in-v0";
4 | import { cn } from "@/lib/utils";
5 | import React from "react";
6 |
7 | interface ComponentWrapperProps extends React.HTMLAttributes {
8 | name: string;
9 | }
10 |
11 | export const ComponentWrapper = ({
12 | className,
13 | children,
14 | name,
15 | }: ComponentWrapperProps) => {
16 | return (
17 |
23 |
24 |
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 lakshaybhushan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/code-block-with-copy.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { CopyButton } from "@/components/copy-button";
5 |
6 | interface CodeBlockWithCopyProps {
7 | children: React.ReactNode;
8 | }
9 |
10 | export function CodeBlockWithCopy({ children }: CodeBlockWithCopyProps) {
11 | const codeRef = React.useRef(null);
12 | const [codeContent, setCodeContent] = React.useState("");
13 |
14 | React.useEffect(() => {
15 | if (codeRef.current) {
16 | const codeElement = codeRef.current.querySelector("code");
17 |
18 | if (codeElement) {
19 | const rawText = codeElement.textContent || "";
20 | setCodeContent(rawText);
21 | } else {
22 | const content = codeRef.current.textContent || "";
23 | setCodeContent(content);
24 | }
25 | }
26 | }, [children]);
27 |
28 | return (
29 |
30 |
31 |
36 |
37 | {children}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/components/blur-bottom.tsx:
--------------------------------------------------------------------------------
1 | export function BlurBottom() {
2 | return (
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import { Provider } from "jotai";
4 | import "./globals.css";
5 | import { BlurBottom } from "@/components/blur-bottom";
6 | import Script from "next/script";
7 |
8 | const geistSans = Geist({
9 | variable: "--font-geist-sans",
10 | subsets: ["latin"],
11 | });
12 |
13 | const geistMono = Geist_Mono({
14 | variable: "--font-geist-mono",
15 | subsets: ["latin"],
16 | });
17 |
18 | export const metadata: Metadata = {
19 | title: "Metal Buttons",
20 | description:
21 | "A beautiful, customizable metal button component with tactile feedback!",
22 | };
23 |
24 | export default function RootLayout({
25 | children,
26 | }: Readonly<{
27 | children: React.ReactNode;
28 | }>) {
29 | return (
30 |
31 |
36 |
39 |
40 | {children}
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/open-in-v0.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 |
3 | export function OpenInV0Button({ url }: { url: string }) {
4 | return (
5 |
10 |
15 | Open in{" "}
16 |
22 |
26 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/sync-mdx.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | const sourceFilePath = path.join(
6 | process.cwd(),
7 | "registry/metal-button/metal-button.tsx",
8 | );
9 | const targetFilePath = path.join(process.cwd(), "lib/metal-source.mdx");
10 |
11 | function readFile(filePath: string): Promise {
12 | return new Promise((resolve, reject) => {
13 | fs.readFile(filePath, "utf8", (err, data) => {
14 | if (err) {
15 | reject(err);
16 | return;
17 | }
18 | resolve(data);
19 | });
20 | });
21 | }
22 |
23 | function writeFile(filePath: string, content: string): Promise {
24 | return new Promise((resolve, reject) => {
25 | fs.writeFile(filePath, content, "utf8", (err) => {
26 | if (err) {
27 | reject(err);
28 | return;
29 | }
30 | resolve();
31 | });
32 | });
33 | }
34 |
35 | async function syncMetalButton(): Promise {
36 | try {
37 | console.log("🔄 Syncing metal-button.tsx to metal-source.mdx...");
38 |
39 | const tsxContent = await readFile(sourceFilePath);
40 |
41 | const mdxContent = await readFile(targetFilePath);
42 |
43 | const mdxPattern = /```tsx\n([\s\S]*?)```/;
44 | const mdxMatch = mdxContent.match(mdxPattern);
45 |
46 | if (!mdxMatch) {
47 | throw new Error("Could not find the code block in the MDX file");
48 | }
49 |
50 | const updatedMdxContent = mdxContent.replace(
51 | mdxPattern,
52 | `\`\`\`tsx\n${tsxContent}\`\`\``,
53 | );
54 |
55 | await writeFile(targetFilePath, updatedMdxContent);
56 |
57 | console.log(
58 | "✅ Successfully updated metal-source.mdx with the content from metal-button.tsx",
59 | );
60 | } catch (error) {
61 | console.error("❌ Error updating the MDX file:", error);
62 | process.exit(1);
63 | }
64 | }
65 |
66 | // Run the sync function
67 | syncMetalButton();
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui-things",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "registry:build": "shadcn build && tsx sync-mdx.ts",
11 | "sync": "tsx sync-mdx.ts"
12 | },
13 | "dependencies": {
14 | "@mdx-js/loader": "^3.1.0",
15 | "@mdx-js/react": "^3.1.0",
16 | "@next/mdx": "^15.2.4",
17 | "@radix-ui/react-accordion": "^1.2.3",
18 | "@radix-ui/react-collapsible": "^1.1.3",
19 | "@radix-ui/react-dropdown-menu": "^2.1.6",
20 | "@radix-ui/react-slot": "^1.1.2",
21 | "@radix-ui/react-tabs": "^1.1.3",
22 | "@types/mdx": "^2.0.13",
23 | "@types/unist": "^3.0.3",
24 | "class-variance-authority": "^0.7.1",
25 | "clsx": "^2.1.1",
26 | "jotai": "^2.12.2",
27 | "lucide-react": "^0.485.0",
28 | "next": "15.2.4",
29 | "react": "^19.0.0",
30 | "react-dom": "^19.0.0",
31 | "react-icons": "^5.5.0",
32 | "rehype-autolink-headings": "^7.1.0",
33 | "rehype-pretty-code": "^0.14.1",
34 | "rehype-slug": "^6.0.0",
35 | "remark-code-import": "^1.2.0",
36 | "remark-gfm": "^4.0.1",
37 | "shadcn": "^2.4.0-canary.20",
38 | "shiki": "^3.2.1",
39 | "tailwind-merge": "^3.0.2",
40 | "tw-animate-css": "^1.2.5",
41 | "unist-builder": "^4.0.0",
42 | "unist-util-visit": "^5.0.0"
43 | },
44 | "devDependencies": {
45 | "@eslint/eslintrc": "^3",
46 | "@tailwindcss/postcss": "^4",
47 | "@tailwindcss/typography": "^0.5.16",
48 | "@types/node": "^20",
49 | "@types/react": "18",
50 | "@types/react-dom": "18",
51 | "@types/react-syntax-highlighter": "^15.5.13",
52 | "eslint": "^9",
53 | "eslint-config-next": "15.2.4",
54 | "prettier": "^3.5.3",
55 | "prettier-plugin-tailwindcss": "^0.6.11",
56 | "tailwindcss": "^4",
57 | "tsx": "^4.19.3",
58 | "typescript": "^5"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Metal Buttons
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## Installation
17 |
18 | For npm or yarn:
19 |
20 | ```sh
21 | npx shadcn@latest add https://button.lakshb.dev/r/metal-button.json
22 | ```
23 |
24 | For pnpm:
25 |
26 | ```sh
27 | pnpm dlx shadcn@latest add https://button.lakshb.dev/r/metal-button.json
28 | ```
29 |
30 | For bun:
31 |
32 | ```sh
33 | bunx --bun shadcn@latest add https://button.lakshb.dev/r/metal-button.json
34 | ```
35 |
36 | ## Usage
37 |
38 | ```tsx
39 | import { MetalButton } from "@/components/metal-button";
40 | ```
41 |
42 | ```tsx
43 | Default
44 | ```
45 |
46 | ## License
47 |
48 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
49 |
50 | ## Contributing
51 |
52 | If you have any suggestions or improvements, please create an issue or a pull request. I'll try to respond to all issues and pull requests.
53 |
54 | ## Support
55 |
56 | If you like this and other projects, you can sponsor me on [GitHub](https://github.com/sponsors/lakshaybhushan) or
57 | [buying me a coffee](https://www.buymeacoffee.com/lakshaybhushan).
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/registry/[name]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import path from "path";
3 | import { promises as fs } from "fs";
4 | import { registryItemSchema } from "shadcn/registry";
5 |
6 | // This route shows an example for serving a component using a route handler.
7 | export async function GET(
8 | request: Request,
9 | { params }: { params: Promise<{ name: string }> },
10 | ) {
11 | try {
12 | const { name } = await params;
13 | // Cache the registry import
14 | const registryData = await import("@/registry.json");
15 | const registry = registryData.default;
16 |
17 | // Find the component from the registry.
18 | const component = registry.items.find((c) => c.name === name);
19 |
20 | // If the component is not found, return a 404 error.
21 | if (!component) {
22 | return NextResponse.json(
23 | { error: "Component not found" },
24 | { status: 404 },
25 | );
26 | }
27 |
28 | // Validate before file operations.
29 | const registryItem = registryItemSchema.parse(component);
30 |
31 | // If the component has no files, return a 400 error.
32 | if (!registryItem.files?.length) {
33 | return NextResponse.json(
34 | { error: "Component has no files" },
35 | { status: 400 },
36 | );
37 | }
38 |
39 | // Read all files in parallel.
40 | const filesWithContent = await Promise.all(
41 | registryItem.files.map(async (file) => {
42 | const filePath = path.join(process.cwd(), file.path);
43 | const content = await fs.readFile(filePath, "utf8");
44 | return { ...file, content };
45 | }),
46 | );
47 |
48 | // Return the component with the files.
49 | return NextResponse.json({ ...registryItem, files: filesWithContent });
50 | } catch (error) {
51 | console.error("Error processing component request:", error);
52 | return NextResponse.json(
53 | { error: "Something went wrong" },
54 | { status: 500 },
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Accordion = AccordionPrimitive.Root;
9 |
10 | const AccordionItem = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
19 | ));
20 | AccordionItem.displayName = "AccordionItem";
21 |
22 | const AccordionTrigger = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef
25 | >(({ className, children, ...props }, ref) => (
26 |
27 | svg]:rotate-180",
31 | className,
32 | )}
33 | {...props}
34 | >
35 | {children}
36 |
37 |
38 | ));
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ));
53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
54 |
55 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
56 |
--------------------------------------------------------------------------------
/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/code-block-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | Collapsible,
9 | CollapsibleContent,
10 | CollapsibleTrigger,
11 | } from "@/components/ui/collapsible";
12 |
13 | interface CodeBlockProps extends React.HTMLAttributes {
14 | expandButtonTitle?: string;
15 | }
16 |
17 | function BlurBottom() {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export function CodeBlockWrapper({
33 | expandButtonTitle = "View Code",
34 | className,
35 | children,
36 | ...props
37 | }: CodeBlockProps) {
38 | const [isOpened, setIsOpened] = React.useState(false);
39 |
40 | return (
41 |
42 |
43 |
47 |
53 | {children}
54 |
55 |
56 | {!isOpened &&
}
57 |
63 |
64 |
65 | {isOpened ? "Collapse" : expandButtonTitle}
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/code-block-command.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { copyToClipboardWithMeta } from "@/components/copy-button";
4 | import { Button } from "@/components/ui/button";
5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6 | import { useConfig } from "@/hooks/config";
7 | import { useMounted } from "@/hooks/use-mounted";
8 | import { NpmCommands } from "@/types/unist";
9 | import { CheckIcon, ClipboardIcon } from "lucide-react";
10 | import * as React from "react";
11 |
12 | export function CodeBlockCommand({
13 | __npmCommand__,
14 | __yarnCommand__,
15 | __pnpmCommand__,
16 | __bunCommand__,
17 | }: React.ComponentProps<"pre"> & NpmCommands) {
18 | const [config, setConfig] = useConfig();
19 | const [hasCopied, setHasCopied] = React.useState(false);
20 | const mounted = useMounted();
21 |
22 | React.useEffect(() => {
23 | if (hasCopied) {
24 | const timer = setTimeout(() => setHasCopied(false), 2000);
25 | return () => clearTimeout(timer);
26 | }
27 | }, [hasCopied]);
28 |
29 | const packageManager = config.packageManager || "pnpm";
30 | const tabs = React.useMemo(() => {
31 | return {
32 | pnpm: __pnpmCommand__,
33 | npm: __npmCommand__,
34 | yarn: __yarnCommand__,
35 | bun: __bunCommand__,
36 | };
37 | }, [__npmCommand__, __pnpmCommand__, __yarnCommand__, __bunCommand__]);
38 |
39 | const copyCommand = React.useCallback(() => {
40 | const command = tabs[packageManager];
41 |
42 | if (!command) {
43 | return;
44 | }
45 |
46 | copyToClipboardWithMeta(command);
47 | setHasCopied(true);
48 | }, [packageManager, tabs]);
49 |
50 | if (!mounted) {
51 | return null;
52 | }
53 |
54 | return (
55 |
56 |
{
59 | setConfig({
60 | ...config,
61 | packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
62 | });
63 | }}
64 | >
65 |
66 |
67 | {Object.entries(tabs).map(([key]) => {
68 | return (
69 |
74 | {key}
75 |
76 | );
77 | })}
78 |
79 |
80 | {Object.entries(tabs).map(([key, value]) => {
81 | return (
82 |
83 |
84 |
88 | {value}
89 |
90 |
91 |
92 | );
93 | })}
94 |
95 |
101 | Copy
102 | {hasCopied ? : }
103 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | rainbow:
23 | "group relative animate-rainbow cursor-pointer border-0 bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] bg-[length:200%] text-primary-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] before:[filter:blur(calc(0.8*1rem))] dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))]",
24 | "rainbow-outline":
25 | "group relative animate-rainbow cursor-pointer border-0 border-input bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] bg-[length:200%] px-4 text-foreground shadow-sm [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] before:[filter:blur(calc(0.8*1rem))] dark:bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))]",
26 | },
27 | size: {
28 | default: "h-9 px-4 py-2",
29 | sm: "h-8 rounded-xl px-3 text-xs",
30 | lg: "h-11 rounded-xl px-8",
31 | icon: "size-9",
32 | },
33 | },
34 | defaultVariants: {
35 | variant: "default",
36 | size: "default",
37 | },
38 | },
39 | );
40 |
41 | export interface ButtonProps
42 | extends React.ButtonHTMLAttributes,
43 | VariantProps {
44 | asChild?: boolean;
45 | }
46 |
47 | const Button = React.forwardRef(
48 | ({ className, variant, size, asChild = false, ...props }, ref) => {
49 | const Comp = asChild ? Slot : "button";
50 | return (
51 |
56 | );
57 | },
58 | );
59 | Button.displayName = "Button";
60 |
61 | export { Button, buttonVariants };
62 |
--------------------------------------------------------------------------------
/registry.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry.json",
3 | "name": "lakssh",
4 | "homepage": "https://button.lakshb.dev",
5 | "items": [
6 | {
7 | "name": "metal-button",
8 | "type": "registry:ui",
9 | "author": "Lakshay Bhushan",
10 | "title": "Metal Button",
11 | "description": "A shadcn/ui based button but it's made of metal",
12 | "files": [
13 | {
14 | "path": "registry/metal-button/metal-button.tsx",
15 | "type": "registry:ui",
16 | "target": "components/metal-button.tsx"
17 | }
18 | ]
19 | },
20 | {
21 | "name": "metal-button-primary",
22 | "type": "registry:ui",
23 | "author": "Lakshay Bhushan",
24 | "title": "Metal Button Primary",
25 | "description": "A shadcn/ui based button but it's made of metal and it's blue",
26 | "registryDependencies": ["https://button.lakshb.dev/r/metal-button.json"],
27 |
28 | "files": [
29 | {
30 | "path": "registry/example/metal-button-primary.tsx",
31 | "type": "registry:ui",
32 | "target": "components/metal-button-primary.tsx"
33 | }
34 | ]
35 | },
36 | {
37 | "name": "metal-button-success",
38 | "type": "registry:ui",
39 | "author": "Lakshay Bhushan",
40 | "title": "Metal Button Success",
41 | "description": "A shadcn/ui based button but it's made of metal and it's green",
42 | "registryDependencies": ["https://button.lakshb.dev/r/metal-button.json"],
43 |
44 | "files": [
45 | {
46 | "path": "registry/example/metal-button-success.tsx",
47 | "type": "registry:ui",
48 | "target": "components/metal-button-success.tsx"
49 | }
50 | ]
51 | },
52 | {
53 | "name": "metal-button-error",
54 | "type": "registry:ui",
55 | "author": "Lakshay Bhushan",
56 | "title": "Metal Button Error",
57 | "description": "A shadcn/ui based button but it's made of metal and it's red",
58 | "registryDependencies": ["https://button.lakshb.dev/r/metal-button.json"],
59 |
60 | "files": [
61 | {
62 | "path": "registry/example/metal-button-error.tsx",
63 | "type": "registry:ui",
64 | "target": "components/metal-button-error.tsx"
65 | }
66 | ]
67 | },
68 | {
69 | "name": "metal-button-gold",
70 | "type": "registry:ui",
71 | "author": "Lakshay Bhushan",
72 | "title": "Metal Button Gold",
73 | "description": "A shadcn/ui based button but it's made of gold metal",
74 | "registryDependencies": ["https://button.lakshb.dev/r/metal-button.json"],
75 |
76 | "files": [
77 | {
78 | "path": "registry/example/metal-button-gold.tsx",
79 | "type": "registry:ui",
80 | "target": "components/metal-button-gold.tsx"
81 | }
82 | ]
83 | },
84 | {
85 | "name": "metal-button-bronze",
86 | "type": "registry:ui",
87 | "author": "Lakshay Bhushan",
88 | "title": "Metal Button Bronze",
89 | "description": "A shadcn/ui based button but it's made of bronze metal",
90 | "registryDependencies": ["https://button.lakshb.dev/r/metal-button.json"],
91 |
92 | "files": [
93 | {
94 | "path": "registry/example/metal-button-bronze.tsx",
95 | "type": "registry:ui",
96 | "target": "components/metal-button-bronze.tsx"
97 | }
98 | ]
99 | },
100 | {
101 | "name": "metal-button-demo",
102 | "type": "registry:page",
103 | "title": "Metal Button Basic Example",
104 | "description": "A basic example of the metal button component",
105 | "author": "Lakshay Bhushan",
106 | "registryDependencies": ["https://button.lakshb.dev/r/metal-button.json"],
107 | "files": [
108 | {
109 | "path": "registry/example/metal-button-demo.tsx",
110 | "type": "registry:page",
111 | "target": "components/metal-button-demo.tsx"
112 | }
113 | ]
114 | }
115 | ]
116 | }
117 |
--------------------------------------------------------------------------------
/app/page.mdx:
--------------------------------------------------------------------------------
1 | import { ComponentPreview } from "../components/component-preview";
2 | import { ComponentSource } from "../components/component-source";
3 | import { CodeBlockCommand } from "../components/code-block-command";
4 | import MetalButtonSource from "../lib/metal-source.mdx";
5 | import { GithubBtn } from "@/components/github-btn";
6 | import {
7 | npmCommand,
8 | yarnCommand,
9 | pnpmCommand,
10 | bunCommand,
11 | metalButtonSourceCode,
12 | } from "../lib/utils";
13 |
14 | # Metal
15 |
16 |
17 |
18 | # Buttons
19 |
20 | ### A beautiful, customizable metal button component with tactile feedback!
21 |
22 | ##### By [lakshaybhushan](https://x.com/blakssh) :)
23 |
24 |
25 | ```tsx
26 | import { MetalButton } from "@/components/metal-button";
27 |
28 | export function MetalButtonDemo() {
29 | return (
30 |
31 | Button
32 |
33 | );
34 | }
35 | ```
36 |
37 |
38 | ## Installation
39 |
40 |
41 |
42 |
43 | CLI
44 | Manual
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 | #### Copy and paste the following code into your project.
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | ## Usage
70 |
71 | ```tsx
72 | import { MetalButton } from "@/components/metal-button";
73 | ```
74 |
75 | ```tsx
76 | Button
77 | ```
78 |
79 | ## Examples
80 |
81 | #### Default
82 |
83 |
84 | ```tsx
85 | import { MetalButton } from "@/components/metal-button";
86 |
87 | export function MetalButtonDefault() {
88 | return (
89 |
90 | Default
91 |
92 | );
93 | }
94 | ```
95 |
96 |
97 | #### Primary
98 |
99 |
100 | ```tsx
101 | import { MetalButton } from "@/components/metal-button";
102 |
103 | export function MetalButtonPrimary() {
104 | return (
105 |
106 | Primary
107 |
108 | );
109 | }
110 | ```
111 |
112 |
113 | #### Success
114 |
115 |
116 | ```tsx
117 | import { MetalButton } from "@/components/metal-button";
118 |
119 | export function MetalButtonSuccess() {
120 | return (
121 |
122 | Success
123 |
124 | );
125 | }
126 | ```
127 |
128 |
129 | #### Error
130 |
131 |
132 | ```tsx
133 | import { MetalButton } from "@/components/metal-button";
134 |
135 | export function MetalButtonError() {
136 | return (
137 |
138 | Error
139 |
140 | );
141 | }
142 | ```
143 |
144 |
145 | #### Gold
146 |
147 |
148 | ```tsx
149 | import { MetalButton } from "@/components/metal-button";
150 |
151 | export function MetalButtonGold() {
152 | return (
153 |
154 | Gold
155 |
156 | );
157 | }
158 | ```
159 |
160 |
161 | #### Bronze
162 |
163 |
164 | ```tsx
165 | import { MetalButton } from "@/components/metal-button";
166 |
167 | export function MetalButtonBronze() {
168 | return (
169 |
170 | Bronze
171 |
172 | );
173 | }
174 | ```
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/components/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu";
5 | import { CheckIcon, ClipboardIcon } from "lucide-react";
6 | import { NpmCommands } from "@/types/unist";
7 |
8 | import { cn } from "@/lib/utils";
9 | import { Button, ButtonProps } from "@/components/ui/button";
10 | import {
11 | DropdownMenu,
12 | DropdownMenuContent,
13 | DropdownMenuItem,
14 | DropdownMenuTrigger,
15 | } from "@/components/dropdown-menu";
16 |
17 | interface CopyButtonProps extends ButtonProps {
18 | value: string;
19 | }
20 |
21 | export async function copyToClipboardWithMeta(value: string) {
22 | await navigator.clipboard.writeText(value);
23 | }
24 |
25 | export function CopyButton({
26 | value,
27 | className,
28 | variant = "ghost",
29 | ...props
30 | }: CopyButtonProps) {
31 | const [hasCopied, setHasCopied] = React.useState(false);
32 |
33 | React.useEffect(() => {
34 | if (hasCopied) {
35 | const timer = setTimeout(() => {
36 | setHasCopied(false);
37 | }, 2000);
38 | return () => clearTimeout(timer);
39 | }
40 | }, [hasCopied]);
41 |
42 | return (
43 | {
51 | copyToClipboardWithMeta(value);
52 | setHasCopied(true);
53 | }}
54 | {...props}
55 | >
56 | Copy
57 | {hasCopied ? : }
58 |
59 | );
60 | }
61 |
62 | interface CopyWithClassNamesProps extends DropdownMenuTriggerProps {
63 | value: string;
64 | classNames: string;
65 | className?: string;
66 | }
67 |
68 | export function CopyWithClassNames({
69 | value,
70 | classNames,
71 | className,
72 | }: CopyWithClassNamesProps) {
73 | const [hasCopied, setHasCopied] = React.useState(false);
74 |
75 | React.useEffect(() => {
76 | if (hasCopied) {
77 | const timer = setTimeout(() => {
78 | setHasCopied(false);
79 | }, 2000);
80 | return () => clearTimeout(timer);
81 | }
82 | }, [hasCopied]);
83 |
84 | const copyToClipboard = React.useCallback((value: string) => {
85 | copyToClipboardWithMeta(value);
86 | setHasCopied(true);
87 | }, []);
88 |
89 | return (
90 |
91 |
92 |
100 | {hasCopied ? (
101 |
102 | ) : (
103 |
104 | )}
105 | Copy
106 |
107 |
108 |
109 | copyToClipboard(value)}>
110 | Component
111 |
112 | copyToClipboard(classNames)}>
113 | Classname
114 |
115 |
116 |
117 | );
118 | }
119 |
120 | interface CopyNpmCommandButtonProps extends DropdownMenuTriggerProps {
121 | commands: Required;
122 | }
123 |
124 | export function CopyNpmCommandButton({
125 | commands,
126 | className,
127 | }: CopyNpmCommandButtonProps) {
128 | const [hasCopied, setHasCopied] = React.useState(false);
129 |
130 | React.useEffect(() => {
131 | if (hasCopied) {
132 | const timer = setTimeout(() => {
133 | setHasCopied(false);
134 | }, 2000);
135 | return () => clearTimeout(timer);
136 | }
137 | }, [hasCopied]);
138 |
139 | const copyCommand = React.useCallback((value: string) => {
140 | copyToClipboardWithMeta(value);
141 | setHasCopied(true);
142 | }, []);
143 |
144 | return (
145 |
146 |
147 |
155 | {hasCopied ? (
156 |
157 | ) : (
158 |
159 | )}
160 | Copy
161 |
162 |
163 |
164 | copyCommand(commands.__npmCommand__)}>
165 | npm
166 |
167 | copyCommand(commands.__yarnCommand__)}>
168 | yarn
169 |
170 | copyCommand(commands.__pnpmCommand__)}>
171 | pnpm
172 |
173 | copyCommand(commands.__bunCommand__)}>
174 | bun
175 |
176 |
177 |
178 | );
179 | }
180 |
--------------------------------------------------------------------------------
/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "@/components/ui/accordion";
7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8 | import { cn } from "@/lib/utils";
9 | import Link from "next/link";
10 | import { CodeBlockCommand } from "@/components/code-block-command";
11 | import { ComponentPreview } from "@/components/component-preview";
12 | import { ComponentSource } from "@/components/component-source";
13 | import { CodeBlockWithCopy } from "@/components/code-block-with-copy";
14 | import type { MDXComponents } from "mdx/types";
15 |
16 | const CustomLink = (props: any) => {
17 | const href = props.href;
18 |
19 | if (href.startsWith("/")) {
20 | return (
21 |
22 | {props.children}
23 |
24 | );
25 | }
26 |
27 | if (href.startsWith("#")) {
28 | return ;
29 | }
30 |
31 | return ;
32 | };
33 |
34 | const otherComponents: MDXComponents = {
35 | Accordion,
36 | AccordionContent,
37 | AccordionItem,
38 | AccordionTrigger,
39 | ComponentPreview,
40 | ComponentSource: (props: any) => ,
41 | h1: ({ className, ...props }: React.HTMLAttributes) => (
42 |
49 | ),
50 | h2: ({ className, ...props }: React.HTMLAttributes) => (
51 |
58 | ),
59 | h3: ({ className, ...props }: React.HTMLAttributes) => (
60 |
67 | ),
68 | h4: ({ className, ...props }: React.HTMLAttributes) => (
69 |
76 | ),
77 | h5: ({ className, ...props }: React.HTMLAttributes) => (
78 |
85 | ),
86 | h6: ({ className, ...props }: React.HTMLAttributes) => (
87 |
94 | ),
95 | a: ({ className, ...props }: React.HTMLAttributes) => (
96 |
100 | ),
101 | p: ({ className, ...props }: React.HTMLAttributes) => (
102 |
106 | ),
107 | Tabs: ({ className, ...props }: React.ComponentProps) => (
108 |
109 | ),
110 | TabsList: ({
111 | className,
112 | ...props
113 | }: React.ComponentProps) => (
114 |
121 | ),
122 | TabsTrigger: ({
123 | className,
124 | ...props
125 | }: React.ComponentProps) => (
126 |
133 | ),
134 | TabsContent: ({
135 | className,
136 | ...props
137 | }: React.ComponentProps) => (
138 |
145 | ),
146 | pre: ({
147 | className,
148 | __rawString__,
149 | __npmCommand__,
150 | __pnpmCommand__,
151 | __yarnCommand__,
152 | __bunCommand__,
153 | __withMeta__,
154 | __name__,
155 | ...props
156 | }: React.HTMLAttributes & {
157 | __rawString__?: string;
158 | __npmCommand__?: string;
159 | __pnpmCommand__?: string;
160 | __yarnCommand__?: string;
161 | __bunCommand__?: string;
162 | __withMeta__?: boolean;
163 | __name__?: string;
164 | }) => {
165 | if (
166 | __npmCommand__ ||
167 | __yarnCommand__ ||
168 | __pnpmCommand__ ||
169 | __bunCommand__
170 | ) {
171 | return (
172 |
178 | );
179 | }
180 |
181 | return (
182 |
183 |
190 |
191 | );
192 | },
193 | code: ({ className, ...props }: React.HTMLAttributes) => (
194 |
198 | ),
199 | };
200 |
201 | export function useMDXComponents(components: MDXComponents): MDXComponents {
202 | return {
203 | ...components,
204 | ...otherComponents,
205 | };
206 | }
207 |
--------------------------------------------------------------------------------
/public/r/metal-button.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "metal-button",
4 | "type": "registry:ui",
5 | "title": "Metal Button",
6 | "author": "Lakshay Bhushan",
7 | "description": "A shadcn/ui based button but it's made of metal",
8 | "files": [
9 | {
10 | "path": "registry/metal-button/metal-button.tsx",
11 | "content": "\"use client\";\nimport React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype ColorVariant =\n | \"default\"\n | \"primary\"\n | \"success\"\n | \"error\"\n | \"gold\"\n | \"bronze\";\n\ninterface MetalButtonProps\n extends React.ButtonHTMLAttributes {\n variant?: ColorVariant;\n}\n\nconst colorVariants: Record<\n ColorVariant,\n {\n outer: string;\n inner: string;\n button: string;\n textColor: string;\n textShadow: string;\n }\n> = {\n default: {\n outer: \"bg-gradient-to-b from-[#000] to-[#A0A0A0]\",\n inner: \"bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]\",\n button: \"bg-gradient-to-b from-[#B9B9B9] to-[#969696]\",\n textColor: \"text-white\",\n textShadow: \"[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]\",\n },\n primary: {\n outer: \"bg-gradient-to-b from-[#0051B4] to-[#90C2FF]\",\n inner: \"bg-gradient-to-b from-[#C4EBFF] via-[#0B3F89] to-[#A6DDFB]\",\n button: \"bg-gradient-to-b from-[#96C6EA] to-[#2D7CCA]\",\n textColor: \"text-[#FFF7F0]\",\n textShadow: \"[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]\",\n },\n success: {\n outer: \"bg-gradient-to-b from-[#005A43] to-[#7CCB9B]\",\n inner: \"bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]\",\n button: \"bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]\",\n textColor: \"text-[#FFF7F0]\",\n textShadow: \"[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]\",\n },\n error: {\n outer: \"bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]\",\n inner: \"bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]\",\n button: \"bg-gradient-to-b from-[#F08D8F] to-[#A45253]\",\n textColor: \"text-[#FFF7F0]\",\n textShadow: \"[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]\",\n },\n gold: {\n outer: \"bg-gradient-to-b from-[#917100] to-[#EAD98F]\",\n inner: \"bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]\",\n button: \"bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]\",\n textColor: \"text-[#FFFDE5]\",\n textShadow: \"[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]\",\n },\n bronze: {\n outer: \"bg-gradient-to-b from-[#864813] to-[#E9B486]\",\n inner: \"bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]\",\n button: \"bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]\",\n textColor: \"text-[#FFF7F0]\",\n textShadow: \"[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]\",\n },\n};\n\nconst metalButtonVariants = (\n variant: ColorVariant = \"default\",\n isPressed: boolean,\n isHovered: boolean,\n isTouchDevice: boolean,\n) => {\n const colors = colorVariants[variant];\n const transitionStyle = \"all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)\";\n\n return {\n wrapper: cn(\n \"relative inline-flex transform-gpu rounded-full p-[1.25px] will-change-transform\",\n colors.outer,\n ),\n wrapperStyle: {\n transform: isPressed\n ? \"translateY(2.5px) scale(0.99)\"\n : \"translateY(0) scale(1)\",\n boxShadow: isPressed\n ? \"0 1px 2px rgba(0, 0, 0, 0.15)\"\n : isHovered && !isTouchDevice\n ? \"0 4px 12px rgba(0, 0, 0, 0.12)\"\n : \"0 3px 8px rgba(0, 0, 0, 0.08)\",\n transition: transitionStyle,\n transformOrigin: \"center center\",\n },\n inner: cn(\n \"absolute inset-[1px] transform-gpu rounded-full will-change-transform\",\n colors.inner,\n ),\n innerStyle: {\n transition: transitionStyle,\n transformOrigin: \"center center\",\n filter:\n isHovered && !isPressed && !isTouchDevice ? \"brightness(1.05)\" : \"none\",\n },\n button: cn(\n \"relative z-10 m-[2.5px] inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-full px-6 pt-4 pb-5 text-2xl leading-none font-bold will-change-transform outline-none\",\n colors.button,\n colors.textColor,\n colors.textShadow,\n ),\n buttonStyle: {\n transform: isPressed ? \"scale(0.97)\" : \"scale(1)\",\n transition: transitionStyle,\n transformOrigin: \"center center\",\n filter:\n isHovered && !isPressed && !isTouchDevice ? \"brightness(1.02)\" : \"none\",\n },\n };\n};\n\nconst ShineEffect = ({ isPressed }: { isPressed: boolean }) => {\n return (\n \n );\n};\n\nexport const MetalButton = React.forwardRef<\n HTMLButtonElement,\n MetalButtonProps\n>(({ children, className, variant = \"default\", ...props }, ref) => {\n const [isPressed, setIsPressed] = React.useState(false);\n const [isHovered, setIsHovered] = React.useState(false);\n const [isTouchDevice, setIsTouchDevice] = React.useState(false);\n\n React.useEffect(() => {\n setIsTouchDevice(\"ontouchstart\" in window || navigator.maxTouchPoints > 0);\n }, []);\n\n const buttonText = children || \"Button\";\n const variants = metalButtonVariants(\n variant,\n isPressed,\n isHovered,\n isTouchDevice,\n );\n\n const handleInternalMouseDown = () => {\n setIsPressed(true);\n };\n const handleInternalMouseUp = () => {\n setIsPressed(false);\n };\n const handleInternalMouseLeave = () => {\n setIsPressed(false);\n setIsHovered(false);\n };\n const handleInternalMouseEnter = () => {\n if (!isTouchDevice) {\n setIsHovered(true);\n }\n };\n const handleInternalTouchStart = () => {\n setIsPressed(true);\n };\n const handleInternalTouchEnd = () => {\n setIsPressed(false);\n };\n const handleInternalTouchCancel = () => {\n setIsPressed(false);\n };\n\n return (\n \n
\n
\n \n {buttonText}\n {isHovered && !isPressed && !isTouchDevice && (\n
\n )}\n \n
\n );\n});\n\nMetalButton.displayName = \"MetalButton\";\n",
12 | "type": "registry:ui",
13 | "target": "components/metal-button.tsx"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/components/metal-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { cn } from "@/lib/utils";
4 |
5 | type ColorVariant =
6 | | "default"
7 | | "primary"
8 | | "success"
9 | | "error"
10 | | "gold"
11 | | "bronze";
12 |
13 | interface MetalButtonProps
14 | extends React.ButtonHTMLAttributes {
15 | variant?: ColorVariant;
16 | }
17 |
18 | const colorVariants: Record<
19 | ColorVariant,
20 | {
21 | outer: string;
22 | inner: string;
23 | button: string;
24 | textColor: string;
25 | textShadow: string;
26 | }
27 | > = {
28 | default: {
29 | outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
30 | inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
31 | button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
32 | textColor: "text-white",
33 | textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
34 | },
35 | primary: {
36 | outer: "bg-gradient-to-b from-[#0051B4] to-[#90C2FF]",
37 | inner: "bg-gradient-to-b from-[#C4EBFF] via-[#0B3F89] to-[#A6DDFB]",
38 | button: "bg-gradient-to-b from-[#96C6EA] to-[#2D7CCA]",
39 | textColor: "text-[#FFF7F0]",
40 | textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
41 | },
42 | success: {
43 | outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
44 | inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
45 | button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
46 | textColor: "text-[#FFF7F0]",
47 | textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
48 | },
49 | error: {
50 | outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
51 | inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
52 | button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
53 | textColor: "text-[#FFF7F0]",
54 | textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
55 | },
56 | gold: {
57 | outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
58 | inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
59 | button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
60 | textColor: "text-[#FFFDE5]",
61 | textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
62 | },
63 | bronze: {
64 | outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
65 | inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
66 | button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
67 | textColor: "text-[#FFF7F0]",
68 | textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
69 | },
70 | };
71 |
72 | const metalButtonVariants = (
73 | variant: ColorVariant = "default",
74 | isPressed: boolean,
75 | isHovered: boolean,
76 | isTouchDevice: boolean,
77 | ) => {
78 | const colors = colorVariants[variant];
79 | const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
80 |
81 | return {
82 | wrapper: cn(
83 | "relative inline-flex transform-gpu rounded-full p-[1.25px] will-change-transform",
84 | colors.outer,
85 | ),
86 | wrapperStyle: {
87 | transform: isPressed
88 | ? "translateY(2.5px) scale(0.99)"
89 | : "translateY(0) scale(1)",
90 | boxShadow: isPressed
91 | ? "0 1px 2px rgba(0, 0, 0, 0.15)"
92 | : isHovered && !isTouchDevice
93 | ? "0 4px 12px rgba(0, 0, 0, 0.12)"
94 | : "0 3px 8px rgba(0, 0, 0, 0.08)",
95 | transition: transitionStyle,
96 | transformOrigin: "center center",
97 | },
98 | inner: cn(
99 | "absolute inset-[1px] transform-gpu rounded-full will-change-transform",
100 | colors.inner,
101 | ),
102 | innerStyle: {
103 | transition: transitionStyle,
104 | transformOrigin: "center center",
105 | filter:
106 | isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
107 | },
108 | button: cn(
109 | "relative z-10 m-[2.5px] inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-full px-6 pt-4 pb-5 text-2xl leading-none font-bold will-change-transform outline-none",
110 | colors.button,
111 | colors.textColor,
112 | colors.textShadow,
113 | ),
114 | buttonStyle: {
115 | transform: isPressed ? "scale(0.97)" : "scale(1)",
116 | transition: transitionStyle,
117 | transformOrigin: "center center",
118 | filter:
119 | isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
120 | },
121 | };
122 | };
123 |
124 | const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
125 | return (
126 |
134 | );
135 | };
136 |
137 | export const MetalButton = React.forwardRef<
138 | HTMLButtonElement,
139 | MetalButtonProps
140 | >(({ children, className, variant = "default", ...props }, ref) => {
141 | const [isPressed, setIsPressed] = React.useState(false);
142 | const [isHovered, setIsHovered] = React.useState(false);
143 | const [isTouchDevice, setIsTouchDevice] = React.useState(false);
144 |
145 | React.useEffect(() => {
146 | setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
147 | }, []);
148 |
149 | const buttonText = children || "Button";
150 | const variants = metalButtonVariants(
151 | variant,
152 | isPressed,
153 | isHovered,
154 | isTouchDevice,
155 | );
156 |
157 | const handleInternalMouseDown = () => {
158 | setIsPressed(true);
159 | };
160 | const handleInternalMouseUp = () => {
161 | setIsPressed(false);
162 | };
163 | const handleInternalMouseLeave = () => {
164 | setIsPressed(false);
165 | setIsHovered(false);
166 | };
167 | const handleInternalMouseEnter = () => {
168 | if (!isTouchDevice) {
169 | setIsHovered(true);
170 | }
171 | };
172 | const handleInternalTouchStart = () => {
173 | setIsPressed(true);
174 | };
175 | const handleInternalTouchEnd = () => {
176 | setIsPressed(false);
177 | };
178 | const handleInternalTouchCancel = () => {
179 | setIsPressed(false);
180 | };
181 |
182 | return (
183 |
184 |
185 |
198 |
199 | {buttonText}
200 | {isHovered && !isPressed && !isTouchDevice && (
201 |
202 | )}
203 |
204 |
205 | );
206 | });
207 |
208 | MetalButton.displayName = "MetalButton";
209 |
--------------------------------------------------------------------------------
/registry/metal-button/metal-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { cn } from "@/lib/utils";
4 |
5 | type ColorVariant =
6 | | "default"
7 | | "primary"
8 | | "success"
9 | | "error"
10 | | "gold"
11 | | "bronze";
12 |
13 | interface MetalButtonProps
14 | extends React.ButtonHTMLAttributes {
15 | variant?: ColorVariant;
16 | }
17 |
18 | const colorVariants: Record<
19 | ColorVariant,
20 | {
21 | outer: string;
22 | inner: string;
23 | button: string;
24 | textColor: string;
25 | textShadow: string;
26 | }
27 | > = {
28 | default: {
29 | outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
30 | inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
31 | button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
32 | textColor: "text-white",
33 | textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
34 | },
35 | primary: {
36 | outer: "bg-gradient-to-b from-[#0051B4] to-[#90C2FF]",
37 | inner: "bg-gradient-to-b from-[#C4EBFF] via-[#0B3F89] to-[#A6DDFB]",
38 | button: "bg-gradient-to-b from-[#96C6EA] to-[#2D7CCA]",
39 | textColor: "text-[#FFF7F0]",
40 | textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
41 | },
42 | success: {
43 | outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
44 | inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
45 | button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
46 | textColor: "text-[#FFF7F0]",
47 | textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
48 | },
49 | error: {
50 | outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
51 | inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
52 | button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
53 | textColor: "text-[#FFF7F0]",
54 | textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
55 | },
56 | gold: {
57 | outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
58 | inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
59 | button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
60 | textColor: "text-[#FFFDE5]",
61 | textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
62 | },
63 | bronze: {
64 | outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
65 | inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
66 | button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
67 | textColor: "text-[#FFF7F0]",
68 | textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
69 | },
70 | };
71 |
72 | const metalButtonVariants = (
73 | variant: ColorVariant = "default",
74 | isPressed: boolean,
75 | isHovered: boolean,
76 | isTouchDevice: boolean,
77 | ) => {
78 | const colors = colorVariants[variant];
79 | const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
80 |
81 | return {
82 | wrapper: cn(
83 | "relative inline-flex transform-gpu rounded-full p-[1.25px] will-change-transform",
84 | colors.outer,
85 | ),
86 | wrapperStyle: {
87 | transform: isPressed
88 | ? "translateY(2.5px) scale(0.99)"
89 | : "translateY(0) scale(1)",
90 | boxShadow: isPressed
91 | ? "0 1px 2px rgba(0, 0, 0, 0.15)"
92 | : isHovered && !isTouchDevice
93 | ? "0 4px 12px rgba(0, 0, 0, 0.12)"
94 | : "0 3px 8px rgba(0, 0, 0, 0.08)",
95 | transition: transitionStyle,
96 | transformOrigin: "center center",
97 | },
98 | inner: cn(
99 | "absolute inset-[1px] transform-gpu rounded-full will-change-transform",
100 | colors.inner,
101 | ),
102 | innerStyle: {
103 | transition: transitionStyle,
104 | transformOrigin: "center center",
105 | filter:
106 | isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
107 | },
108 | button: cn(
109 | "relative z-10 m-[2.5px] inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-full px-6 pt-4 pb-5 text-2xl leading-none font-bold will-change-transform outline-none",
110 | colors.button,
111 | colors.textColor,
112 | colors.textShadow,
113 | ),
114 | buttonStyle: {
115 | transform: isPressed ? "scale(0.97)" : "scale(1)",
116 | transition: transitionStyle,
117 | transformOrigin: "center center",
118 | filter:
119 | isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
120 | },
121 | };
122 | };
123 |
124 | const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
125 | return (
126 |
134 | );
135 | };
136 |
137 | export const MetalButton = React.forwardRef<
138 | HTMLButtonElement,
139 | MetalButtonProps
140 | >(({ children, className, variant = "default", ...props }, ref) => {
141 | const [isPressed, setIsPressed] = React.useState(false);
142 | const [isHovered, setIsHovered] = React.useState(false);
143 | const [isTouchDevice, setIsTouchDevice] = React.useState(false);
144 |
145 | React.useEffect(() => {
146 | setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
147 | }, []);
148 |
149 | const buttonText = children || "Button";
150 | const variants = metalButtonVariants(
151 | variant,
152 | isPressed,
153 | isHovered,
154 | isTouchDevice,
155 | );
156 |
157 | const handleInternalMouseDown = () => {
158 | setIsPressed(true);
159 | };
160 | const handleInternalMouseUp = () => {
161 | setIsPressed(false);
162 | };
163 | const handleInternalMouseLeave = () => {
164 | setIsPressed(false);
165 | setIsHovered(false);
166 | };
167 | const handleInternalMouseEnter = () => {
168 | if (!isTouchDevice) {
169 | setIsHovered(true);
170 | }
171 | };
172 | const handleInternalTouchStart = () => {
173 | setIsPressed(true);
174 | };
175 | const handleInternalTouchEnd = () => {
176 | setIsPressed(false);
177 | };
178 | const handleInternalTouchCancel = () => {
179 | setIsPressed(false);
180 | };
181 |
182 | return (
183 |
184 |
185 |
198 |
199 | {buttonText}
200 | {isHovered && !isPressed && !isTouchDevice && (
201 |
202 | )}
203 |
204 |
205 | );
206 | });
207 |
208 | MetalButton.displayName = "MetalButton";
209 |
--------------------------------------------------------------------------------
/lib/metal-source.mdx:
--------------------------------------------------------------------------------
1 | ```tsx
2 | "use client";
3 | import React from "react";
4 | import { cn } from "@/lib/utils";
5 |
6 | type ColorVariant =
7 | | "default"
8 | | "primary"
9 | | "success"
10 | | "error"
11 | | "gold"
12 | | "bronze";
13 |
14 | interface MetalButtonProps
15 | extends React.ButtonHTMLAttributes {
16 | variant?: ColorVariant;
17 | }
18 |
19 | const colorVariants: Record<
20 | ColorVariant,
21 | {
22 | outer: string;
23 | inner: string;
24 | button: string;
25 | textColor: string;
26 | textShadow: string;
27 | }
28 | > = {
29 | default: {
30 | outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
31 | inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
32 | button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
33 | textColor: "text-white",
34 | textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
35 | },
36 | primary: {
37 | outer: "bg-gradient-to-b from-[#0051B4] to-[#90C2FF]",
38 | inner: "bg-gradient-to-b from-[#C4EBFF] via-[#0B3F89] to-[#A6DDFB]",
39 | button: "bg-gradient-to-b from-[#96C6EA] to-[#2D7CCA]",
40 | textColor: "text-[#FFF7F0]",
41 | textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
42 | },
43 | success: {
44 | outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
45 | inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
46 | button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
47 | textColor: "text-[#FFF7F0]",
48 | textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
49 | },
50 | error: {
51 | outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
52 | inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
53 | button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
54 | textColor: "text-[#FFF7F0]",
55 | textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
56 | },
57 | gold: {
58 | outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
59 | inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
60 | button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
61 | textColor: "text-[#FFFDE5]",
62 | textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
63 | },
64 | bronze: {
65 | outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
66 | inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
67 | button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
68 | textColor: "text-[#FFF7F0]",
69 | textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
70 | },
71 | };
72 |
73 | const metalButtonVariants = (
74 | variant: ColorVariant = "default",
75 | isPressed: boolean,
76 | isHovered: boolean,
77 | isTouchDevice: boolean,
78 | ) => {
79 | const colors = colorVariants[variant];
80 | const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
81 |
82 | return {
83 | wrapper: cn(
84 | "relative inline-flex transform-gpu rounded-full p-[1.25px] will-change-transform",
85 | colors.outer,
86 | ),
87 | wrapperStyle: {
88 | transform: isPressed
89 | ? "translateY(2.5px) scale(0.99)"
90 | : "translateY(0) scale(1)",
91 | boxShadow: isPressed
92 | ? "0 1px 2px rgba(0, 0, 0, 0.15)"
93 | : isHovered && !isTouchDevice
94 | ? "0 4px 12px rgba(0, 0, 0, 0.12)"
95 | : "0 3px 8px rgba(0, 0, 0, 0.08)",
96 | transition: transitionStyle,
97 | transformOrigin: "center center",
98 | },
99 | inner: cn(
100 | "absolute inset-[1px] transform-gpu rounded-full will-change-transform",
101 | colors.inner,
102 | ),
103 | innerStyle: {
104 | transition: transitionStyle,
105 | transformOrigin: "center center",
106 | filter:
107 | isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
108 | },
109 | button: cn(
110 | "relative z-10 m-[2.5px] inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-full px-6 pt-4 pb-5 text-2xl leading-none font-bold will-change-transform outline-none",
111 | colors.button,
112 | colors.textColor,
113 | colors.textShadow,
114 | ),
115 | buttonStyle: {
116 | transform: isPressed ? "scale(0.97)" : "scale(1)",
117 | transition: transitionStyle,
118 | transformOrigin: "center center",
119 | filter:
120 | isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
121 | },
122 | };
123 | };
124 |
125 | const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
126 | return (
127 |
135 | );
136 | };
137 |
138 | export const MetalButton = React.forwardRef<
139 | HTMLButtonElement,
140 | MetalButtonProps
141 | >(({ children, className, variant = "default", ...props }, ref) => {
142 | const [isPressed, setIsPressed] = React.useState(false);
143 | const [isHovered, setIsHovered] = React.useState(false);
144 | const [isTouchDevice, setIsTouchDevice] = React.useState(false);
145 |
146 | React.useEffect(() => {
147 | setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
148 | }, []);
149 |
150 | const buttonText = children || "Button";
151 | const variants = metalButtonVariants(
152 | variant,
153 | isPressed,
154 | isHovered,
155 | isTouchDevice,
156 | );
157 |
158 | const handleInternalMouseDown = () => {
159 | setIsPressed(true);
160 | };
161 | const handleInternalMouseUp = () => {
162 | setIsPressed(false);
163 | };
164 | const handleInternalMouseLeave = () => {
165 | setIsPressed(false);
166 | setIsHovered(false);
167 | };
168 | const handleInternalMouseEnter = () => {
169 | if (!isTouchDevice) {
170 | setIsHovered(true);
171 | }
172 | };
173 | const handleInternalTouchStart = () => {
174 | setIsPressed(true);
175 | };
176 | const handleInternalTouchEnd = () => {
177 | setIsPressed(false);
178 | };
179 | const handleInternalTouchCancel = () => {
180 | setIsPressed(false);
181 | };
182 |
183 | return (
184 |
185 |
186 |
199 |
200 | {buttonText}
201 | {isHovered && !isPressed && !isTouchDevice && (
202 |
203 | )}
204 |
205 |
206 | );
207 | });
208 |
209 | MetalButton.displayName = "MetalButton";
210 | ```
211 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 | @plugin "@tailwindcss/typography";
4 |
5 | @custom-variant dark (&:is(.dark *));
6 |
7 | @theme inline {
8 | --color-background: var(--background);
9 | --color-foreground: var(--foreground);
10 | --font-sans: var(--font-geist-sans);
11 | --font-mono: var(--font-geist-mono);
12 | --color-sidebar-ring: var(--sidebar-ring);
13 | --color-sidebar-border: var(--sidebar-border);
14 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
15 | --color-sidebar-accent: var(--sidebar-accent);
16 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
17 | --color-sidebar-primary: var(--sidebar-primary);
18 | --color-sidebar-foreground: var(--sidebar-foreground);
19 | --color-sidebar: var(--sidebar);
20 | --color-chart-5: var(--chart-5);
21 | --color-chart-4: var(--chart-4);
22 | --color-chart-3: var(--chart-3);
23 | --color-chart-2: var(--chart-2);
24 | --color-chart-1: var(--chart-1);
25 | --color-ring: var(--ring);
26 | --color-input: var(--input);
27 | --color-border: var(--border);
28 | --color-destructive: var(--destructive);
29 | --color-accent-foreground: var(--accent-foreground);
30 | --color-accent: var(--accent);
31 | --color-muted-foreground: var(--muted-foreground);
32 | --color-muted: var(--muted);
33 | --color-secondary-foreground: var(--secondary-foreground);
34 | --color-secondary: var(--secondary);
35 | --color-primary-foreground: var(--primary-foreground);
36 | --color-primary: var(--primary);
37 | --color-popover-foreground: var(--popover-foreground);
38 | --color-popover: var(--popover);
39 | --color-card-foreground: var(--card-foreground);
40 | --color-card: var(--card);
41 | --radius-sm: calc(var(--radius) - 4px);
42 | --radius-md: calc(var(--radius) - 2px);
43 | --radius-lg: var(--radius);
44 | --radius-xl: calc(var(--radius) + 4px);
45 | }
46 |
47 | :root {
48 | --radius: 0.625rem;
49 | --background: oklch(1 0 0);
50 | --foreground: oklch(0.145 0 0);
51 | --card: oklch(1 0 0);
52 | --card-foreground: oklch(0.145 0 0);
53 | --popover: oklch(1 0 0);
54 | --popover-foreground: oklch(0.145 0 0);
55 | --primary: oklch(0.205 0 0);
56 | --primary-foreground: oklch(0.985 0 0);
57 | --secondary: oklch(0.97 0 0);
58 | --secondary-foreground: oklch(0.205 0 0);
59 | --muted: oklch(0.97 0 0);
60 | --muted-foreground: oklch(0.556 0 0);
61 | --accent: oklch(0.97 0 0);
62 | --accent-foreground: oklch(0.205 0 0);
63 | --destructive: oklch(0.577 0.245 27.325);
64 | --border: oklch(0.922 0 0);
65 | --input: oklch(0.922 0 0);
66 | --ring: oklch(0.708 0 0);
67 | --chart-1: oklch(0.646 0.222 41.116);
68 | --chart-2: oklch(0.6 0.118 184.704);
69 | --chart-3: oklch(0.398 0.07 227.392);
70 | --chart-4: oklch(0.828 0.189 84.429);
71 | --chart-5: oklch(0.769 0.188 70.08);
72 | --sidebar: oklch(0.985 0 0);
73 | --sidebar-foreground: oklch(0.145 0 0);
74 | --sidebar-primary: oklch(0.205 0 0);
75 | --sidebar-primary-foreground: oklch(0.985 0 0);
76 | --sidebar-accent: oklch(0.97 0 0);
77 | --sidebar-accent-foreground: oklch(0.205 0 0);
78 | --sidebar-border: oklch(0.922 0 0);
79 | --sidebar-ring: oklch(0.708 0 0);
80 |
81 | }
82 |
83 | .dark {
84 | --background: oklch(0.145 0 0);
85 | --foreground: oklch(0.985 0 0);
86 | --card: oklch(0.205 0 0);
87 | --card-foreground: oklch(0.985 0 0);
88 | --popover: oklch(0.205 0 0);
89 | --popover-foreground: oklch(0.985 0 0);
90 | --primary: oklch(0.922 0 0);
91 | --primary-foreground: oklch(0.205 0 0);
92 | --secondary: oklch(0.269 0 0);
93 | --secondary-foreground: oklch(0.985 0 0);
94 | --muted: oklch(0.269 0 0);
95 | --muted-foreground: oklch(0.708 0 0);
96 | --accent: oklch(0.269 0 0);
97 | --accent-foreground: oklch(0.985 0 0);
98 | --destructive: oklch(0.704 0.191 22.216);
99 | --border: oklch(1 0 0 / 10%);
100 | --input: oklch(1 0 0 / 15%);
101 | --ring: oklch(0.556 0 0);
102 | --chart-1: oklch(0.488 0.243 264.376);
103 | --chart-2: oklch(0.696 0.17 162.48);
104 | --chart-3: oklch(0.769 0.188 70.08);
105 | --chart-4: oklch(0.627 0.265 303.9);
106 | --chart-5: oklch(0.645 0.246 16.439);
107 | --sidebar: oklch(0.205 0 0);
108 | --sidebar-foreground: oklch(0.985 0 0);
109 | --sidebar-primary: oklch(0.488 0.243 264.376);
110 | --sidebar-primary-foreground: oklch(0.985 0 0);
111 | --sidebar-accent: oklch(0.269 0 0);
112 | --sidebar-accent-foreground: oklch(0.985 0 0);
113 | --sidebar-border: oklch(1 0 0 / 10%);
114 | --sidebar-ring: oklch(0.556 0 0);
115 | }
116 |
117 | @layer base {
118 | * {
119 | @apply border-border outline-ring/50;
120 | }
121 | body {
122 | @apply bg-background text-foreground;
123 | }
124 | }
125 |
126 | ::selection {
127 | @apply bg-blue-600 text-white;
128 | }
129 |
130 | .dark ::selection {
131 | @apply bg-blue-600 text-white;
132 | }
133 |
134 |
135 | /* Mask gradients for blur effect */
136 | .mask-gradient-1 {
137 | -webkit-mask-image: linear-gradient(
138 | to bottom,
139 | rgba(0, 0, 0, 0) 0%,
140 | rgba(0, 0, 0, 1) 12.5%,
141 | rgba(0, 0, 0, 1) 25%,
142 | rgba(0, 0, 0, 0) 37.5%
143 | );
144 | mask-image: linear-gradient(
145 | to bottom,
146 | rgba(0, 0, 0, 0) 0%,
147 | rgba(0, 0, 0, 1) 12.5%,
148 | rgba(0, 0, 0, 1) 25%,
149 | rgba(0, 0, 0, 0) 37.5%
150 | );
151 | }
152 |
153 | .mask-gradient-2 {
154 | -webkit-mask-image: linear-gradient(
155 | to bottom,
156 | rgba(0, 0, 0, 0) 12.5%,
157 | rgba(0, 0, 0, 1) 25%,
158 | rgba(0, 0, 0, 1) 37.5%,
159 | rgba(0, 0, 0, 0) 50%
160 | );
161 | mask-image: linear-gradient(
162 | to bottom,
163 | rgba(0, 0, 0, 0) 12.5%,
164 | rgba(0, 0, 0, 1) 25%,
165 | rgba(0, 0, 0, 1) 37.5%,
166 | rgba(0, 0, 0, 0) 50%
167 | );
168 | }
169 |
170 | .mask-gradient-3 {
171 | -webkit-mask-image: linear-gradient(
172 | to bottom,
173 | rgba(0, 0, 0, 0) 25%,
174 | rgba(0, 0, 0, 1) 37.5%,
175 | rgba(0, 0, 0, 1) 50%,
176 | rgba(0, 0, 0, 0) 62.5%
177 | );
178 | mask-image: linear-gradient(
179 | to bottom,
180 | rgba(0, 0, 0, 0) 25%,
181 | rgba(0, 0, 0, 1) 37.5%,
182 | rgba(0, 0, 0, 1) 50%,
183 | rgba(0, 0, 0, 0) 62.5%
184 | );
185 | }
186 |
187 | .mask-gradient-4 {
188 | -webkit-mask-image: linear-gradient(
189 | to bottom,
190 | rgba(0, 0, 0, 0) 37.5%,
191 | rgba(0, 0, 0, 1) 50%,
192 | rgba(0, 0, 0, 1) 62.5%,
193 | rgba(0, 0, 0, 0) 75%
194 | );
195 | mask-image: linear-gradient(
196 | to bottom,
197 | rgba(0, 0, 0, 0) 37.5%,
198 | rgba(0, 0, 0, 1) 50%,
199 | rgba(0, 0, 0, 1) 62.5%,
200 | rgba(0, 0, 0, 0) 75%
201 | );
202 | }
203 |
204 | .mask-gradient-5 {
205 | -webkit-mask-image: linear-gradient(
206 | to bottom,
207 | rgba(0, 0, 0, 0) 50%,
208 | rgba(0, 0, 0, 1) 62.5%,
209 | rgba(0, 0, 0, 1) 75%,
210 | rgba(0, 0, 0, 0) 87.5%
211 | );
212 | mask-image: linear-gradient(
213 | to bottom,
214 | rgba(0, 0, 0, 0) 50%,
215 | rgba(0, 0, 0, 1) 62.5%,
216 | rgba(0, 0, 0, 1) 75%,
217 | rgba(0, 0, 0, 0) 87.5%
218 | );
219 | }
220 |
221 | .mask-gradient-6 {
222 | -webkit-mask-image: linear-gradient(
223 | to bottom,
224 | rgba(0, 0, 0, 0) 62.5%,
225 | rgba(0, 0, 0, 1) 75%,
226 | rgba(0, 0, 0, 1) 87.5%,
227 | rgba(0, 0, 0, 0) 100%
228 | );
229 | mask-image: linear-gradient(
230 | to bottom,
231 | rgba(0, 0, 0, 0) 62.5%,
232 | rgba(0, 0, 0, 1) 75%,
233 | rgba(0, 0, 0, 1) 87.5%,
234 | rgba(0, 0, 0, 0) 100%
235 | );
236 | }
237 |
238 | .mask-gradient-7 {
239 | -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 1) 87.5%, rgba(0, 0, 0, 1) 100%);
240 | mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 1) 87.5%, rgba(0, 0, 0, 1) 100%);
241 | }
242 |
243 | .mask-gradient-8 {
244 | -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, rgba(0, 0, 0, 1) 100%);
245 | mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, rgba(0, 0, 0, 1) 100%);
246 | }
247 |
--------------------------------------------------------------------------------
/components/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 { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | function DropdownMenu({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function DropdownMenuPortal({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | );
21 | }
22 |
23 | function DropdownMenuTrigger({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
31 | );
32 | }
33 |
34 | function DropdownMenuContent({
35 | className,
36 | sideOffset = 4,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
41 |
50 |
51 | );
52 | }
53 |
54 | function DropdownMenuGroup({
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
59 | );
60 | }
61 |
62 | function DropdownMenuItem({
63 | className,
64 | inset,
65 | variant = "default",
66 | ...props
67 | }: React.ComponentProps & {
68 | inset?: boolean;
69 | variant?: "default" | "destructive";
70 | }) {
71 | return (
72 |
82 | );
83 | }
84 |
85 | function DropdownMenuCheckboxItem({
86 | className,
87 | children,
88 | checked,
89 | ...props
90 | }: React.ComponentProps) {
91 | return (
92 |
101 |
102 |
103 |
104 |
105 |
106 | {children}
107 |
108 | );
109 | }
110 |
111 | function DropdownMenuRadioGroup({
112 | ...props
113 | }: React.ComponentProps) {
114 | return (
115 |
119 | );
120 | }
121 |
122 | function DropdownMenuRadioItem({
123 | className,
124 | children,
125 | ...props
126 | }: React.ComponentProps) {
127 | return (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | );
144 | }
145 |
146 | function DropdownMenuLabel({
147 | className,
148 | inset,
149 | ...props
150 | }: React.ComponentProps & {
151 | inset?: boolean;
152 | }) {
153 | return (
154 |
163 | );
164 | }
165 |
166 | function DropdownMenuSeparator({
167 | className,
168 | ...props
169 | }: React.ComponentProps) {
170 | return (
171 |
176 | );
177 | }
178 |
179 | function DropdownMenuShortcut({
180 | className,
181 | ...props
182 | }: React.ComponentProps<"span">) {
183 | return (
184 |
192 | );
193 | }
194 |
195 | function DropdownMenuSub({
196 | ...props
197 | }: React.ComponentProps) {
198 | return ;
199 | }
200 |
201 | function DropdownMenuSubTrigger({
202 | className,
203 | inset,
204 | children,
205 | ...props
206 | }: React.ComponentProps & {
207 | inset?: boolean;
208 | }) {
209 | return (
210 |
219 | {children}
220 |
221 |
222 | );
223 | }
224 |
225 | function DropdownMenuSubContent({
226 | className,
227 | ...props
228 | }: React.ComponentProps) {
229 | return (
230 |
238 | );
239 | }
240 |
241 | export {
242 | DropdownMenu,
243 | DropdownMenuPortal,
244 | DropdownMenuTrigger,
245 | DropdownMenuContent,
246 | DropdownMenuGroup,
247 | DropdownMenuLabel,
248 | DropdownMenuItem,
249 | DropdownMenuCheckboxItem,
250 | DropdownMenuRadioGroup,
251 | DropdownMenuRadioItem,
252 | DropdownMenuSeparator,
253 | DropdownMenuShortcut,
254 | DropdownMenuSub,
255 | DropdownMenuSubTrigger,
256 | DropdownMenuSubContent,
257 | };
258 |
--------------------------------------------------------------------------------
/components/component-preview.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { cn } from "@/lib/utils";
5 | import { Loader2 } from "lucide-react";
6 | import { ComponentWrapper } from "@/components/component-wrapper";
7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8 | import { CodeBlockWithCopy } from "@/components/code-block-with-copy";
9 |
10 | interface ComponentEntry {
11 | name: string;
12 | description: string;
13 | type: string;
14 | author: string;
15 | registryDependencies: string[];
16 | files: {
17 | path: string;
18 | type: string;
19 | target: string;
20 | }[];
21 | component: React.LazyExoticComponent;
22 | meta: undefined;
23 | }
24 |
25 | const Index: Record = {
26 | "metal-button-demo": {
27 | name: "metal-button-demo",
28 | description: "A basic example of the metal button component",
29 | type: "registry:page",
30 | author: "Lakshay Bhushan",
31 | registryDependencies: ["https://button.lakshb.dev/r/metal-button.json"],
32 | files: [
33 | {
34 | path: "registry/example/metal-button-demo.tsx",
35 | type: "registry:page",
36 | target: "components/metal-button-demo.tsx",
37 | },
38 | ],
39 | component: React.lazy(async () => {
40 | try {
41 | const mod = await import("@/registry/example/metal-button-demo");
42 | return { default: mod.MetalButtonDemo };
43 | } catch (error) {
44 | console.error("Error loading component:", error);
45 | return {
46 | default: () => Error loading component
,
47 | };
48 | }
49 | }),
50 | meta: undefined,
51 | },
52 | "metal-button-primary": {
53 | name: "metal-button-primary",
54 | description: "Primary variant of the metal button component",
55 | type: "registry:page",
56 | author: "Lakshay Bhushan",
57 | registryDependencies: ["https://button.lakshb.dev/r/metal-button.json"],
58 | files: [
59 | {
60 | path: "registry/example/metal-button-primary.tsx",
61 | type: "registry:page",
62 | target: "components/metal-button-primary.tsx",
63 | },
64 | ],
65 | component: React.lazy(async () => {
66 | try {
67 | const mod = await import("@/registry/example/metal-button-primary");
68 | return { default: mod.MetalButtonPrimary };
69 | } catch (error) {
70 | console.error("Error loading component:", error);
71 | return {
72 | default: () => Error loading component
,
73 | };
74 | }
75 | }),
76 | meta: undefined,
77 | },
78 | "metal-button-success": {
79 | name: "metal-button-success",
80 | description: "Success variant of the metal button component",
81 | type: "registry:page",
82 | author: "Lakshay Bhushan",
83 | registryDependencies: ["https://button.lakshb.dev/r/metal-button.json"],
84 | files: [
85 | {
86 | path: "registry/example/metal-button-success.tsx",
87 | type: "registry:page",
88 | target: "components/metal-button-success.tsx",
89 | },
90 | ],
91 | component: React.lazy(async () => {
92 | try {
93 | const mod = await import("@/registry/example/metal-button-success");
94 | return { default: mod.MetalButtonSuccess };
95 | } catch (error) {
96 | console.error("Error loading component:", error);
97 | return {
98 | default: () => Error loading component
,
99 | };
100 | }
101 | }),
102 | meta: undefined,
103 | },
104 | "metal-button-error": {
105 | name: "metal-button-error",
106 | description: "Error variant of the metal button component",
107 | type: "registry:page",
108 | author: "Lakshay Bhushan",
109 | registryDependencies: ["https://button.lakshb.dev/r/metal-button.json"],
110 | files: [
111 | {
112 | path: "registry/example/metal-button-error.tsx",
113 | type: "registry:page",
114 | target: "components/metal-button-error.tsx",
115 | },
116 | ],
117 | component: React.lazy(async () => {
118 | try {
119 | const mod = await import("@/registry/example/metal-button-error");
120 | return { default: mod.MetalButtonError };
121 | } catch (error) {
122 | console.error("Error loading component:", error);
123 | return {
124 | default: () => Error loading component
,
125 | };
126 | }
127 | }),
128 | meta: undefined,
129 | },
130 | "metal-button-gold": {
131 | name: "metal-button-gold",
132 | description: "Gold variant of the metal button component",
133 | type: "registry:page",
134 | author: "Lakshay Bhushan",
135 | registryDependencies: ["https://button.lakshb.dev/r/metal-button.json"],
136 | files: [
137 | {
138 | path: "registry/example/metal-button-gold.tsx",
139 | type: "registry:page",
140 | target: "components/metal-button-gold.tsx",
141 | },
142 | ],
143 | component: React.lazy(async () => {
144 | try {
145 | const mod = await import("@/registry/example/metal-button-gold");
146 | return { default: mod.MetalButtonGold };
147 | } catch (error) {
148 | console.error("Error loading component:", error);
149 | return {
150 | default: () => Error loading component
,
151 | };
152 | }
153 | }),
154 | meta: undefined,
155 | },
156 | "metal-button-bronze": {
157 | name: "metal-button-bronze",
158 | description: "Bronze variant of the metal button component",
159 | type: "registry:page",
160 | author: "Lakshay Bhushan",
161 | registryDependencies: ["https://button.lakshb.dev/r/metal-button.json"],
162 | files: [
163 | {
164 | path: "registry/example/metal-button-bronze.tsx",
165 | type: "registry:page",
166 | target: "components/metal-button-bronze.tsx",
167 | },
168 | ],
169 | component: React.lazy(async () => {
170 | try {
171 | const mod = await import("@/registry/example/metal-button-bronze");
172 | return { default: mod.MetalButtonBronze };
173 | } catch (error) {
174 | console.error("Error loading component:", error);
175 | return {
176 | default: () => Error loading component
,
177 | };
178 | }
179 | }),
180 | meta: undefined,
181 | },
182 | };
183 |
184 | interface ComponentPreviewProps extends React.HTMLAttributes {
185 | name: string;
186 | preview?: boolean;
187 | }
188 |
189 | export function ComponentPreview({
190 | name,
191 | children,
192 | className,
193 | preview = false,
194 | ...props
195 | }: ComponentPreviewProps) {
196 | const Codes = React.Children.toArray(children) as React.ReactElement[];
197 | const Code = Codes.length > 0 ? Codes[0] : null;
198 |
199 | const Preview = React.useMemo(() => {
200 | const Component = Index[name]?.component;
201 |
202 | if (!Component) {
203 | console.error(`Component with name "${name}" not found in registry.`);
204 | return (
205 |
206 | Component{" "}
207 |
208 | {name}
209 | {" "}
210 | not found in registry.
211 |
212 | );
213 | }
214 |
215 | return ;
216 | }, [name]);
217 |
218 | return (
219 |
226 |
227 | {!preview && (
228 |
229 |
230 |
234 | Preview
235 |
236 |
240 | Code
241 |
242 |
243 |
244 | )}
245 |
246 |
247 |
250 |
251 |
252 | }
253 | >
254 |
255 | {Preview}
256 |
257 |
258 |
259 |
260 |
261 |
262 | {Code ? (
263 |
264 |
265 | {Code}
266 |
267 |
268 | ) : (
269 |
270 | No code example provided
271 |
272 | )}
273 |
274 |
275 |
276 |
277 | );
278 | }
279 |
--------------------------------------------------------------------------------