├── 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 | 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 |