diff --git a/app/showcase/components/copyable-demo.tsx b/app/showcase/components/copyable-demo.tsx
new file mode 100644
index 0000000..28370df
--- /dev/null
+++ b/app/showcase/components/copyable-demo.tsx
@@ -0,0 +1,49 @@
+import {
+ Copyable,
+ CopyableButton,
+ CopyableContent,
+} from "@registry/bases/base/ui/copyable";
+
+import { Example } from "./example";
+
+// The hero — the canonical Copyable use case: a CLI install command sitting in a
+// card surface with a copy button that confirms with a check.
+export function CopyablePreview() {
+ return (
+
+ npx shadcn@latest add @crescent-ui/copyable
+
+
+ );
+}
+
+// The additional usages, each its own flat example.
+export function CopyableExamples() {
+ return (
+
+ );
+}
diff --git a/app/showcase/components/demos.tsx b/app/showcase/components/demos.tsx
index a567204..4fedd26 100644
--- a/app/showcase/components/demos.tsx
+++ b/app/showcase/components/demos.tsx
@@ -1,4 +1,5 @@
import { ChoiceboxExamples, ChoiceboxPreview } from "./choicebox-demo";
+import { CopyableExamples, CopyablePreview } from "./copyable-demo";
// The gallery's source of truth: one entry per published crescent-ui component.
// `preview` is the hero shown in the preview frame; `examples` are the extra
@@ -22,6 +23,14 @@ export const demos: Record<
preview: ,
examples: ,
},
+ copyable: {
+ title: "Copyable",
+ description:
+ "A card-styled surface that displays a value alongside a button that copies it to the clipboard and confirms with a check.",
+ sourceFile: "ui/copyable.tsx",
+ preview: ,
+ examples: ,
+ },
};
export const demoOrder = Object.keys(demos);
diff --git a/registry-dist/base-luma/copyable.json b/registry-dist/base-luma/copyable.json
new file mode 100644
index 0000000..aee1da4
--- /dev/null
+++ b/registry-dist/base-luma/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-luma/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-lyra/copyable.json b/registry-dist/base-lyra/copyable.json
new file mode 100644
index 0000000..90d1aa6
--- /dev/null
+++ b/registry-dist/base-lyra/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-lyra/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-maia/copyable.json b/registry-dist/base-maia/copyable.json
new file mode 100644
index 0000000..fbb52ba
--- /dev/null
+++ b/registry-dist/base-maia/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-maia/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-mira/copyable.json b/registry-dist/base-mira/copyable.json
new file mode 100644
index 0000000..2c9ce1f
--- /dev/null
+++ b/registry-dist/base-mira/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-mira/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-nova/copyable.json b/registry-dist/base-nova/copyable.json
new file mode 100644
index 0000000..b0d7013
--- /dev/null
+++ b/registry-dist/base-nova/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-nova/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-rhea/copyable.json b/registry-dist/base-rhea/copyable.json
new file mode 100644
index 0000000..853a4ff
--- /dev/null
+++ b/registry-dist/base-rhea/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-rhea/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-sera/copyable.json b/registry-dist/base-sera/copyable.json
new file mode 100644
index 0000000..27f888e
--- /dev/null
+++ b/registry-dist/base-sera/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-sera/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/base-vega/copyable.json b/registry-dist/base-vega/copyable.json
new file mode 100644
index 0000000..6bcc090
--- /dev/null
+++ b/registry-dist/base-vega/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/base-vega/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-luma/copyable.json b/registry-dist/radix-luma/copyable.json
new file mode 100644
index 0000000..5dd5be3
--- /dev/null
+++ b/registry-dist/radix-luma/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-luma/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-lyra/copyable.json b/registry-dist/radix-lyra/copyable.json
new file mode 100644
index 0000000..fc91438
--- /dev/null
+++ b/registry-dist/radix-lyra/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-lyra/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-maia/copyable.json b/registry-dist/radix-maia/copyable.json
new file mode 100644
index 0000000..e37b248
--- /dev/null
+++ b/registry-dist/radix-maia/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-maia/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-mira/copyable.json b/registry-dist/radix-mira/copyable.json
new file mode 100644
index 0000000..e21280c
--- /dev/null
+++ b/registry-dist/radix-mira/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-mira/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-nova/copyable.json b/registry-dist/radix-nova/copyable.json
new file mode 100644
index 0000000..8875ee8
--- /dev/null
+++ b/registry-dist/radix-nova/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-nova/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-rhea/copyable.json b/registry-dist/radix-rhea/copyable.json
new file mode 100644
index 0000000..f5daed7
--- /dev/null
+++ b/registry-dist/radix-rhea/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-rhea/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-sera/copyable.json b/registry-dist/radix-sera/copyable.json
new file mode 100644
index 0000000..4081a36
--- /dev/null
+++ b/registry-dist/radix-sera/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-sera/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry-dist/radix-vega/copyable.json b/registry-dist/radix-vega/copyable.json
new file mode 100644
index 0000000..7239004
--- /dev/null
+++ b/registry-dist/radix-vega/copyable.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/radix-vega/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx",
+ "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconPlaceholder } from \"@/ui/icon-placeholder\";\n\n// Copyable — a card-styled \"value + copy button\" surface: a visualization of\n// some text (an API key, an install command, a token) paired with a button that\n// copies it to the clipboard and confirms with a check.\n//\n// Copyable has no Base UI / Radix primitive dependency — it's structural markup\n// plus the Clipboard API — so the `base` and `radix` variants are byte-identical\n// and kept in sync only so each project base can install it from its namespace.\n//\n// STYLING MODEL (shadcn cn-* system):\n// The visual treatment of each part lives in `cn-copyable-*` semantic classes,\n// defined per style under registry/styles/style-.css via `@apply`. Only\n// structural layout (flex/positioning/behavior) is inline here. The registry\n// build resolves each `cn-*` token into that style's utilities (see\n// scripts/build-registry.ts); the showcase resolves them live via a\n// `.style-` wrapper class. Keep ALL color/border/radius/size/typography\n// in the style CSS — inline classes win twMerge conflicts and would override it.\n//\n// Anatomy (compound):\n// \n// npm i crescent-ui\n// \n// \n//\n// `value` is the string written to the clipboard; the displayed content is\n// whatever you put in (usually the same text, but it can be a\n// masked or prettier representation of the value).\n\ntype CopyableContextValue = {\n copied: boolean;\n copy: () => void;\n};\n\nconst CopyableContext = React.createContext(null);\n\nfunction useCopyable() {\n const context = React.useContext(CopyableContext);\n if (!context) {\n throw new Error(\"Copyable parts must be used within .\");\n }\n return context;\n}\n\nfunction Copyable({\n value,\n timeout = 2000,\n onCopy,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n /** The string written to the clipboard when the button is pressed. */\n value: string;\n /** How long (ms) the \"copied\" confirmation stays before resetting. */\n timeout?: number;\n /** Called after a successful copy. */\n onCopy?: (value: string) => void;\n}) {\n const [copied, setCopied] = React.useState(false);\n\n const copy = React.useCallback(() => {\n // Guard for non-secure contexts / older browsers where the async Clipboard\n // API is unavailable — fail silently rather than throwing in the handler.\n if (!navigator.clipboard?.writeText) return;\n void navigator.clipboard\n .writeText(value)\n .then(() => {\n setCopied(true);\n onCopy?.(value);\n })\n .catch(() => {});\n }, [value, onCopy]);\n\n // Reset the confirmation after `timeout`; re-copying restarts the timer.\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), timeout);\n return () => window.clearTimeout(id);\n }, [copied, timeout]);\n\n const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);\n\n return (\n \n \n \n );\n}\n\nfunction CopyableContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction CopyableButton({\n className,\n children,\n ...props\n}: React.ComponentProps<\"button\">) {\n const { copied, copy } = useCopyable();\n return (\n \n );\n}\n\nexport { Copyable, CopyableContent, CopyableButton, useCopyable };\n"
+ }
+ ]
+}
diff --git a/registry.json b/registry.json
index fa227f3..9996b6b 100644
--- a/registry.json
+++ b/registry.json
@@ -30,6 +30,34 @@
"target": "components/ui/choicebox.tsx"
}
]
+ },
+ {
+ "name": "copyable",
+ "type": "registry:ui",
+ "title": "Copyable",
+ "description": "A card-styled surface that displays a value alongside a button that copies it to the clipboard and confirms with a check. Fully theme-token driven and icon-library agnostic.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/bases/base/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx"
+ }
+ ]
+ },
+ {
+ "name": "copyable-radix",
+ "type": "registry:ui",
+ "title": "Copyable (Radix)",
+ "description": "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency). Install this when your project's base is `radix`.",
+ "dependencies": [],
+ "files": [
+ {
+ "path": "registry/bases/radix/ui/copyable.tsx",
+ "type": "registry:ui",
+ "target": "components/ui/copyable.tsx"
+ }
+ ]
}
]
}
diff --git a/registry/bases/base/ui/copyable.tsx b/registry/bases/base/ui/copyable.tsx
new file mode 100644
index 0000000..b909f1f
--- /dev/null
+++ b/registry/bases/base/ui/copyable.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+import { IconPlaceholder } from "@/ui/icon-placeholder";
+
+// Copyable — a card-styled "value + copy button" surface: a visualization of
+// some text (an API key, an install command, a token) paired with a button that
+// copies it to the clipboard and confirms with a check.
+//
+// Copyable has no Base UI / Radix primitive dependency — it's structural markup
+// plus the Clipboard API — so the `base` and `radix` variants are byte-identical
+// and kept in sync only so each project base can install it from its namespace.
+//
+// STYLING MODEL (shadcn cn-* system):
+// The visual treatment of each part lives in `cn-copyable-*` semantic classes,
+// defined per style under registry/styles/style-.css via `@apply`. Only
+// structural layout (flex/positioning/behavior) is inline here. The registry
+// build resolves each `cn-*` token into that style's utilities (see
+// scripts/build-registry.ts); the showcase resolves them live via a
+// `.style-` wrapper class. Keep ALL color/border/radius/size/typography
+// in the style CSS — inline classes win twMerge conflicts and would override it.
+//
+// Anatomy (compound):
+//
+// npm i crescent-ui
+//
+//
+//
+// `value` is the string written to the clipboard; the displayed content is
+// whatever you put in (usually the same text, but it can be a
+// masked or prettier representation of the value).
+
+type CopyableContextValue = {
+ copied: boolean;
+ copy: () => void;
+};
+
+const CopyableContext = React.createContext(null);
+
+function useCopyable() {
+ const context = React.useContext(CopyableContext);
+ if (!context) {
+ throw new Error("Copyable parts must be used within .");
+ }
+ return context;
+}
+
+function Copyable({
+ value,
+ timeout = 2000,
+ onCopy,
+ className,
+ ...props
+}: React.ComponentProps<"div"> & {
+ /** The string written to the clipboard when the button is pressed. */
+ value: string;
+ /** How long (ms) the "copied" confirmation stays before resetting. */
+ timeout?: number;
+ /** Called after a successful copy. */
+ onCopy?: (value: string) => void;
+}) {
+ const [copied, setCopied] = React.useState(false);
+
+ const copy = React.useCallback(() => {
+ // Guard for non-secure contexts / older browsers where the async Clipboard
+ // API is unavailable — fail silently rather than throwing in the handler.
+ if (!navigator.clipboard?.writeText) return;
+ void navigator.clipboard
+ .writeText(value)
+ .then(() => {
+ setCopied(true);
+ onCopy?.(value);
+ })
+ .catch(() => {});
+ }, [value, onCopy]);
+
+ // Reset the confirmation after `timeout`; re-copying restarts the timer.
+ React.useEffect(() => {
+ if (!copied) return;
+ const id = window.setTimeout(() => setCopied(false), timeout);
+ return () => window.clearTimeout(id);
+ }, [copied, timeout]);
+
+ const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);
+
+ return (
+
+
+
+ );
+}
+
+function CopyableContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CopyableButton({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"button">) {
+ const { copied, copy } = useCopyable();
+ return (
+
+ );
+}
+
+export { Copyable, CopyableContent, CopyableButton, useCopyable };
diff --git a/registry/bases/radix/ui/copyable.tsx b/registry/bases/radix/ui/copyable.tsx
new file mode 100644
index 0000000..b909f1f
--- /dev/null
+++ b/registry/bases/radix/ui/copyable.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+import { IconPlaceholder } from "@/ui/icon-placeholder";
+
+// Copyable — a card-styled "value + copy button" surface: a visualization of
+// some text (an API key, an install command, a token) paired with a button that
+// copies it to the clipboard and confirms with a check.
+//
+// Copyable has no Base UI / Radix primitive dependency — it's structural markup
+// plus the Clipboard API — so the `base` and `radix` variants are byte-identical
+// and kept in sync only so each project base can install it from its namespace.
+//
+// STYLING MODEL (shadcn cn-* system):
+// The visual treatment of each part lives in `cn-copyable-*` semantic classes,
+// defined per style under registry/styles/style-.css via `@apply`. Only
+// structural layout (flex/positioning/behavior) is inline here. The registry
+// build resolves each `cn-*` token into that style's utilities (see
+// scripts/build-registry.ts); the showcase resolves them live via a
+// `.style-` wrapper class. Keep ALL color/border/radius/size/typography
+// in the style CSS — inline classes win twMerge conflicts and would override it.
+//
+// Anatomy (compound):
+//
+// npm i crescent-ui
+//
+//
+//
+// `value` is the string written to the clipboard; the displayed content is
+// whatever you put in (usually the same text, but it can be a
+// masked or prettier representation of the value).
+
+type CopyableContextValue = {
+ copied: boolean;
+ copy: () => void;
+};
+
+const CopyableContext = React.createContext(null);
+
+function useCopyable() {
+ const context = React.useContext(CopyableContext);
+ if (!context) {
+ throw new Error("Copyable parts must be used within .");
+ }
+ return context;
+}
+
+function Copyable({
+ value,
+ timeout = 2000,
+ onCopy,
+ className,
+ ...props
+}: React.ComponentProps<"div"> & {
+ /** The string written to the clipboard when the button is pressed. */
+ value: string;
+ /** How long (ms) the "copied" confirmation stays before resetting. */
+ timeout?: number;
+ /** Called after a successful copy. */
+ onCopy?: (value: string) => void;
+}) {
+ const [copied, setCopied] = React.useState(false);
+
+ const copy = React.useCallback(() => {
+ // Guard for non-secure contexts / older browsers where the async Clipboard
+ // API is unavailable — fail silently rather than throwing in the handler.
+ if (!navigator.clipboard?.writeText) return;
+ void navigator.clipboard
+ .writeText(value)
+ .then(() => {
+ setCopied(true);
+ onCopy?.(value);
+ })
+ .catch(() => {});
+ }, [value, onCopy]);
+
+ // Reset the confirmation after `timeout`; re-copying restarts the timer.
+ React.useEffect(() => {
+ if (!copied) return;
+ const id = window.setTimeout(() => setCopied(false), timeout);
+ return () => window.clearTimeout(id);
+ }, [copied, timeout]);
+
+ const context = React.useMemo(() => ({ copied, copy }), [copied, copy]);
+
+ return (
+
+
+
+ );
+}
+
+function CopyableContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CopyableButton({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"button">) {
+ const { copied, copy } = useCopyable();
+ return (
+
+ );
+}
+
+export { Copyable, CopyableContent, CopyableButton, useCopyable };
diff --git a/registry/styles/style-luma.css b/registry/styles/style-luma.css
index 3f93fad..c6d3b49 100644
--- a/registry/styles/style-luma.css
+++ b/registry/styles/style-luma.css
@@ -34,6 +34,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Pillowy: very round, soft shadow. */
+ .cn-copyable {
+ @apply gap-2 rounded-3xl border border-transparent bg-muted/50 px-4 py-2.5 text-foreground shadow-sm;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-sm text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-9 rounded-full bg-transparent text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary;
+ }
+
.cn-choicebox-item-indicator {
@apply size-5 rounded-full data-[multiple]:rounded-lg border border-input bg-background text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground;
}
diff --git a/registry/styles/style-lyra.css b/registry/styles/style-lyra.css
index 05ddbe7..e5a9997 100644
--- a/registry/styles/style-lyra.css
+++ b/registry/styles/style-lyra.css
@@ -33,6 +33,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Compact, square, hairline focus. */
+ .cn-copyable {
+ @apply gap-1.5 rounded-none border border-input bg-card px-2.5 py-1.5 text-card-foreground;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-xs text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-7 rounded-none bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring/50 data-copied:text-primary;
+ }
+
.cn-choicebox-item-indicator {
@apply size-4 rounded-full data-[multiple]:rounded-none border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground;
}
diff --git a/registry/styles/style-maia.css b/registry/styles/style-maia.css
index c1d8683..a6a17c3 100644
--- a/registry/styles/style-maia.css
+++ b/registry/styles/style-maia.css
@@ -33,6 +33,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Very round, pill copy button. */
+ .cn-copyable {
+ @apply gap-2 rounded-2xl border border-input bg-card px-4 py-2.5 text-card-foreground shadow-xs;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-sm text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-9 rounded-full bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/30 data-copied:text-primary;
+ }
+
.cn-choicebox-item-indicator {
@apply size-5 rounded-full data-[multiple]:rounded-lg border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground;
}
diff --git a/registry/styles/style-mira.css b/registry/styles/style-mira.css
index e16d88f..1964606 100644
--- a/registry/styles/style-mira.css
+++ b/registry/styles/style-mira.css
@@ -44,6 +44,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Same soft, rounded temperament. */
+ .cn-copyable {
+ @apply gap-2 rounded-lg border border-input bg-card px-3 py-2 text-card-foreground shadow-xs;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-sm text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-8 rounded-md bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-copied:text-primary;
+ }
+
/* Radio (single) stays an intrinsic circle; checkbox (multiple) tracks the
soft radius. data-multiple is set by the component. */
.cn-choicebox-item-indicator {
diff --git a/registry/styles/style-nova.css b/registry/styles/style-nova.css
index f1f30f4..aa9ea70 100644
--- a/registry/styles/style-nova.css
+++ b/registry/styles/style-nova.css
@@ -32,6 +32,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Flat tonal fill, no border. */
+ .cn-copyable {
+ @apply gap-2 rounded-lg border border-transparent bg-muted/40 px-3 py-2 text-foreground;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-sm text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-8 rounded-lg bg-transparent text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary;
+ }
+
.cn-choicebox-item-indicator {
@apply size-5 rounded-full data-[multiple]:rounded-sm border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground;
}
diff --git a/registry/styles/style-rhea.css b/registry/styles/style-rhea.css
index cbff236..56a3ac8 100644
--- a/registry/styles/style-rhea.css
+++ b/registry/styles/style-rhea.css
@@ -33,6 +33,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Generously round, squircle button. */
+ .cn-copyable {
+ @apply gap-2 rounded-2xl border border-transparent bg-muted/40 px-4 py-2.5 text-foreground shadow-sm;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-sm text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-9 rounded-xl bg-transparent text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary;
+ }
+
.cn-choicebox-item-indicator {
@apply size-5 rounded-full data-[multiple]:rounded-lg border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground;
}
diff --git a/registry/styles/style-sera.css b/registry/styles/style-sera.css
index 80fac8c..0ae5aee 100644
--- a/registry/styles/style-sera.css
+++ b/registry/styles/style-sera.css
@@ -39,6 +39,20 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Sharp, editorial: the button is a
+ bordered square that inverts to a solid foreground block on copy. */
+ .cn-copyable {
+ @apply gap-3 rounded-none border border-input bg-transparent px-4 py-2.5 text-foreground;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-xs text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-9 rounded-none border border-input bg-transparent text-foreground transition-colors hover:bg-foreground hover:text-background focus-visible:ring-2 focus-visible:ring-ring/30 data-copied:border-foreground data-copied:bg-foreground data-copied:text-background;
+ }
+
/* Radio stays circular; checkbox is a hard square. Selected fills with the
foreground and knocks the glyph out in the background color. */
.cn-choicebox-item-indicator {
diff --git a/registry/styles/style-vega.css b/registry/styles/style-vega.css
index 4ef7507..e713f64 100644
--- a/registry/styles/style-vega.css
+++ b/registry/styles/style-vega.css
@@ -32,6 +32,19 @@
@apply text-xs/relaxed text-muted-foreground;
}
+ /* Copyable — a value surface + copy button. Bordered card, medium radius. */
+ .cn-copyable {
+ @apply gap-2 rounded-xl border border-input bg-card px-3 py-2 text-card-foreground shadow-xs;
+ }
+
+ .cn-copyable-content {
+ @apply truncate font-mono text-sm text-foreground;
+ }
+
+ .cn-copyable-button {
+ @apply size-8 rounded-lg bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/30 data-copied:text-primary;
+ }
+
.cn-choicebox-item-indicator {
@apply size-5 rounded-full data-[multiple]:rounded-sm border border-input text-transparent transition-colors group-data-pressed/choicebox-item:border-primary group-data-pressed/choicebox-item:bg-primary group-data-pressed/choicebox-item:text-primary-foreground group-data-[state=on]/choicebox-item:border-primary group-data-[state=on]/choicebox-item:bg-primary group-data-[state=on]/choicebox-item:text-primary-foreground;
}
diff --git a/scripts/build-registry.ts b/scripts/build-registry.ts
index f2cc629..dc82925 100644
--- a/scripts/build-registry.ts
+++ b/scripts/build-registry.ts
@@ -61,6 +61,11 @@ const CHOICEBOX_BASE_DESCRIPTION =
const CHOICEBOX_RADIX_DESCRIPTION =
"The Radix variant of Choicebox, built on the Radix Toggle Group (type=\"single\" | \"multiple\").";
+const COPYABLE_BASE_DESCRIPTION =
+ "A card-styled surface that displays a value alongside a button that copies it to the clipboard.";
+const COPYABLE_RADIX_DESCRIPTION =
+ "The Radix variant of Copyable — byte-identical to the Base UI variant (Copyable has no primitive dependency).";
+
const BASES: Base[] = [
{
name: "base",
@@ -74,6 +79,15 @@ const BASES: Base[] = [
file: "ui/choicebox.tsx",
target: "components/ui/choicebox.tsx",
},
+ {
+ name: "copyable",
+ type: "registry:ui",
+ title: "Copyable",
+ description: COPYABLE_BASE_DESCRIPTION,
+ dependencies: [],
+ file: "ui/copyable.tsx",
+ target: "components/ui/copyable.tsx",
+ },
],
},
{
@@ -88,6 +102,15 @@ const BASES: Base[] = [
file: "ui/choicebox.tsx",
target: "components/ui/choicebox.tsx",
},
+ {
+ name: "copyable",
+ type: "registry:ui",
+ title: "Copyable (Radix)",
+ description: COPYABLE_RADIX_DESCRIPTION,
+ dependencies: [],
+ file: "ui/copyable.tsx",
+ target: "components/ui/copyable.tsx",
+ },
],
},
];