@zentauri-ui/zentauri-components 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +8 -4
  2. package/cli/registry.json +2 -0
  3. package/dist/chunk-7TGUGTTQ.mjs +147 -0
  4. package/dist/chunk-7TGUGTTQ.mjs.map +1 -0
  5. package/dist/chunk-CQMV7BB6.js +50 -0
  6. package/dist/chunk-CQMV7BB6.js.map +1 -0
  7. package/dist/chunk-DN7TYUJ6.js +119 -0
  8. package/dist/chunk-DN7TYUJ6.js.map +1 -0
  9. package/dist/chunk-ODBG4Y6R.mjs +48 -0
  10. package/dist/chunk-ODBG4Y6R.mjs.map +1 -0
  11. package/dist/chunk-RKX5MERK.js +150 -0
  12. package/dist/chunk-RKX5MERK.js.map +1 -0
  13. package/dist/chunk-VYI3GS2C.mjs +115 -0
  14. package/dist/chunk-VYI3GS2C.mjs.map +1 -0
  15. package/dist/design-system/copy-button.d.ts +43 -0
  16. package/dist/design-system/copy-button.d.ts.map +1 -0
  17. package/dist/design-system/index.d.ts +2 -0
  18. package/dist/design-system/index.d.ts.map +1 -1
  19. package/dist/design-system/kbd.d.ts +44 -0
  20. package/dist/design-system/kbd.d.ts.map +1 -0
  21. package/dist/hooks/useClipboard.js +6 -44
  22. package/dist/hooks/useClipboard.js.map +1 -1
  23. package/dist/hooks/useClipboard.mjs +1 -46
  24. package/dist/hooks/useClipboard.mjs.map +1 -1
  25. package/dist/ui/copy-button/animated/animations.d.ts +3 -0
  26. package/dist/ui/copy-button/animated/animations.d.ts.map +1 -0
  27. package/dist/ui/copy-button/animated/copy-button-animated.d.ts +6 -0
  28. package/dist/ui/copy-button/animated/copy-button-animated.d.ts.map +1 -0
  29. package/dist/ui/copy-button/animated/index.d.ts +4 -0
  30. package/dist/ui/copy-button/animated/index.d.ts.map +1 -0
  31. package/dist/ui/copy-button/animated/types.d.ts +26 -0
  32. package/dist/ui/copy-button/animated/types.d.ts.map +1 -0
  33. package/dist/ui/copy-button/animated.js +59 -0
  34. package/dist/ui/copy-button/animated.js.map +1 -0
  35. package/dist/ui/copy-button/animated.mjs +56 -0
  36. package/dist/ui/copy-button/animated.mjs.map +1 -0
  37. package/dist/ui/copy-button/copy-button-base.d.ts +6 -0
  38. package/dist/ui/copy-button/copy-button-base.d.ts.map +1 -0
  39. package/dist/ui/copy-button/copy-button.d.ts +6 -0
  40. package/dist/ui/copy-button/copy-button.d.ts.map +1 -0
  41. package/dist/ui/copy-button/index.d.ts +4 -0
  42. package/dist/ui/copy-button/index.d.ts.map +1 -0
  43. package/dist/ui/copy-button/types.d.ts +32 -0
  44. package/dist/ui/copy-button/types.d.ts.map +1 -0
  45. package/dist/ui/copy-button/variants.d.ts +6 -0
  46. package/dist/ui/copy-button/variants.d.ts.map +1 -0
  47. package/dist/ui/copy-button.js +20 -0
  48. package/dist/ui/copy-button.js.map +1 -0
  49. package/dist/ui/copy-button.mjs +15 -0
  50. package/dist/ui/copy-button.mjs.map +1 -0
  51. package/dist/ui/kbd/animated/animations.d.ts +3 -0
  52. package/dist/ui/kbd/animated/animations.d.ts.map +1 -0
  53. package/dist/ui/kbd/animated/index.d.ts +4 -0
  54. package/dist/ui/kbd/animated/index.d.ts.map +1 -0
  55. package/dist/ui/kbd/animated/kbd-animated.d.ts +6 -0
  56. package/dist/ui/kbd/animated/kbd-animated.d.ts.map +1 -0
  57. package/dist/ui/kbd/animated/types.d.ts +10 -0
  58. package/dist/ui/kbd/animated/types.d.ts.map +1 -0
  59. package/dist/ui/kbd/animated.js +42 -0
  60. package/dist/ui/kbd/animated.js.map +1 -0
  61. package/dist/ui/kbd/animated.mjs +39 -0
  62. package/dist/ui/kbd/animated.mjs.map +1 -0
  63. package/dist/ui/kbd/index.d.ts +4 -0
  64. package/dist/ui/kbd/index.d.ts.map +1 -0
  65. package/dist/ui/kbd/kbd-base.d.ts +6 -0
  66. package/dist/ui/kbd/kbd-base.d.ts.map +1 -0
  67. package/dist/ui/kbd/kbd.d.ts +6 -0
  68. package/dist/ui/kbd/kbd.d.ts.map +1 -0
  69. package/dist/ui/kbd/types.d.ts +17 -0
  70. package/dist/ui/kbd/types.d.ts.map +1 -0
  71. package/dist/ui/kbd/variants.d.ts +8 -0
  72. package/dist/ui/kbd/variants.d.ts.map +1 -0
  73. package/dist/ui/kbd.js +23 -0
  74. package/dist/ui/kbd.js.map +1 -0
  75. package/dist/ui/kbd.mjs +14 -0
  76. package/dist/ui/kbd.mjs.map +1 -0
  77. package/package.json +1 -1
  78. package/src/design-system/copy-button.ts +81 -0
  79. package/src/design-system/index.ts +2 -0
  80. package/src/design-system/kbd.ts +83 -0
  81. package/src/ui/copy-button/animated/animations.ts +22 -0
  82. package/src/ui/copy-button/animated/copy-button-animated.tsx +39 -0
  83. package/src/ui/copy-button/animated/index.ts +10 -0
  84. package/src/ui/copy-button/animated/types.ts +21 -0
  85. package/src/ui/copy-button/copy-button-base.tsx +88 -0
  86. package/src/ui/copy-button/copy-button.test.tsx +82 -0
  87. package/src/ui/copy-button/copy-button.tsx +9 -0
  88. package/src/ui/copy-button/index.ts +10 -0
  89. package/src/ui/copy-button/types.ts +37 -0
  90. package/src/ui/copy-button/variants.ts +29 -0
  91. package/src/ui/kbd/animated/animations.ts +15 -0
  92. package/src/ui/kbd/animated/index.ts +9 -0
  93. package/src/ui/kbd/animated/kbd-animated.tsx +26 -0
  94. package/src/ui/kbd/animated/types.ts +16 -0
  95. package/src/ui/kbd/index.ts +5 -0
  96. package/src/ui/kbd/kbd-base.tsx +50 -0
  97. package/src/ui/kbd/kbd.test.tsx +48 -0
  98. package/src/ui/kbd/kbd.tsx +9 -0
  99. package/src/ui/kbd/types.ts +21 -0
  100. package/src/ui/kbd/variants.ts +31 -0
package/dist/ui/kbd.js ADDED
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var chunkDN7TYUJ6_js = require('../chunk-DN7TYUJ6.js');
5
+ require('../chunk-ZS5756ZC.js');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ function Kbd(props) {
9
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkDN7TYUJ6_js.KbdBase, { ...props });
10
+ }
11
+ Kbd.displayName = "Kbd";
12
+
13
+ Object.defineProperty(exports, "kbdKeyVariants", {
14
+ enumerable: true,
15
+ get: function () { return chunkDN7TYUJ6_js.kbdKeyVariants; }
16
+ });
17
+ Object.defineProperty(exports, "kbdSeparatorVariants", {
18
+ enumerable: true,
19
+ get: function () { return chunkDN7TYUJ6_js.kbdSeparatorVariants; }
20
+ });
21
+ exports.Kbd = Kbd;
22
+ //# sourceMappingURL=kbd.js.map
23
+ //# sourceMappingURL=kbd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ui/kbd/kbd.tsx"],"names":["jsx","KbdBase"],"mappings":";;;;;;AAIO,SAAS,IAAI,KAAA,EAAiB;AACnC,EAAA,uBAAOA,cAAA,CAACC,wBAAA,EAAA,EAAS,GAAG,KAAA,EAAO,CAAA;AAC7B;AAEA,GAAA,CAAI,WAAA,GAAc,KAAA","file":"kbd.js","sourcesContent":["// kbd.tsx — default static entry (no framer-motion)\nimport { KbdBase } from \"./kbd-base\";\nimport type { KbdProps } from \"./types\";\n\nexport function Kbd(props: KbdProps) {\n return <KbdBase {...props} />;\n}\n\nKbd.displayName = \"Kbd\";\n"]}
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { KbdBase } from '../chunk-VYI3GS2C.mjs';
3
+ export { kbdKeyVariants, kbdSeparatorVariants } from '../chunk-VYI3GS2C.mjs';
4
+ import '../chunk-4D54YOL6.mjs';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ function Kbd(props) {
8
+ return /* @__PURE__ */ jsx(KbdBase, { ...props });
9
+ }
10
+ Kbd.displayName = "Kbd";
11
+
12
+ export { Kbd };
13
+ //# sourceMappingURL=kbd.mjs.map
14
+ //# sourceMappingURL=kbd.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/ui/kbd/kbd.tsx"],"names":[],"mappings":";;;;;AAIO,SAAS,IAAI,KAAA,EAAiB;AACnC,EAAA,uBAAO,GAAA,CAAC,OAAA,EAAA,EAAS,GAAG,KAAA,EAAO,CAAA;AAC7B;AAEA,GAAA,CAAI,WAAA,GAAc,KAAA","file":"kbd.mjs","sourcesContent":["// kbd.tsx — default static entry (no framer-motion)\nimport { KbdBase } from \"./kbd-base\";\nimport type { KbdProps } from \"./types\";\n\nexport function Kbd(props: KbdProps) {\n return <KbdBase {...props} />;\n}\n\nKbd.displayName = \"Kbd\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zentauri-ui/zentauri-components",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "React + Tailwind UI kit with charts, ESM/CJS builds, per-entry exports, and a zentauri-components / zentauri-ui CLI to vendor UI or hook source into your app",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -0,0 +1,81 @@
1
+ export const zuiCopyButtonBase = [
2
+ "relative inline-flex items-center justify-center gap-2 whitespace-nowrap",
3
+ "rounded-[var(--zui-copy-button-radius,0.75rem)] font-medium",
4
+ "ring-offset-[var(--zui-copy-button-ring-offset,#f8fafc)] dark:ring-offset-[var(--zui-copy-button-ring-offset-dark,#020617)]",
5
+ "transition-colors select-none",
6
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--zui-copy-button-focus-ring,#475569)] dark:focus-visible:ring-[var(--zui-copy-button-focus-ring-dark,#cbd5e1)] focus-visible:ring-offset-2",
7
+ "disabled:pointer-events-none disabled:opacity-50",
8
+ ] as const;
9
+
10
+ export const zuiCopyButtonAppearances = {
11
+ default:
12
+ "bg-[var(--zui-copy-button-default-bg,#0f172a)] dark:bg-[var(--zui-copy-button-default-bg-dark,#f8fafc)] text-[color:var(--zui-copy-button-default-fg,#f8fafc)] dark:text-[color:var(--zui-copy-button-default-fg-dark,#020617)] shadow-[var(--zui-copy-button-default-shadow,0_1px_2px_#0f172a14)] dark:shadow-[var(--zui-copy-button-default-shadow-dark,0_1px_2px_#0f172a1f)] hover:bg-[var(--zui-copy-button-default-bg-hover,#000000)] dark:hover:bg-[var(--zui-copy-button-default-bg-hover-dark,#ffffff)]",
13
+ secondary:
14
+ "bg-[var(--zui-copy-button-secondary-bg,#e2e8f0)] dark:bg-[var(--zui-copy-button-secondary-bg-dark,#1e293b)] text-[color:var(--zui-copy-button-secondary-fg,#0f172a)] dark:text-[color:var(--zui-copy-button-secondary-fg-dark,#f8fafc)] hover:bg-[var(--zui-copy-button-secondary-bg-hover,#cbd5e1)] dark:hover:bg-[var(--zui-copy-button-secondary-bg-hover-dark,#334155)]",
15
+ destructive:
16
+ "bg-[var(--zui-copy-button-destructive-bg,#f43f5e)] dark:bg-[var(--zui-copy-button-destructive-bg-dark,#be123c)] text-[color:var(--zui-copy-button-destructive-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-destructive-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-destructive-bg-hover,#f43f5e)] dark:hover:bg-[var(--zui-copy-button-destructive-bg-hover-dark,#9f1239)]",
17
+ outline:
18
+ "border border-[color:var(--zui-copy-button-outline-border,#0000001a)] dark:border-[color:var(--zui-copy-button-outline-border-dark,#ffffff1a)] bg-[var(--zui-copy-button-outline-bg,#0000000d)] dark:bg-[var(--zui-copy-button-outline-bg-dark,#ffffff0d)] text-[color:var(--zui-copy-button-outline-fg,#0f172a)] dark:text-[color:var(--zui-copy-button-outline-fg-dark,#f8fafc)] hover:bg-[var(--zui-copy-button-outline-bg-hover,#0000001a)] dark:hover:bg-[var(--zui-copy-button-outline-bg-hover-dark,#ffffff1a)]",
19
+ ghost:
20
+ "bg-transparent text-[color:var(--zui-copy-button-ghost-fg,#334155)] dark:text-[color:var(--zui-copy-button-ghost-fg-dark,#e2e8f0)] hover:bg-[var(--zui-copy-button-ghost-bg-hover,#0000000d)] dark:hover:bg-[var(--zui-copy-button-ghost-bg-hover-dark,#ffffff0d)]",
21
+ glass:
22
+ "border border-[color:var(--zui-copy-button-glass-border,#00000026)] dark:border-[color:var(--zui-copy-button-glass-border-dark,#ffffff26)] bg-[var(--zui-copy-button-glass-bg,#0000001a)] dark:bg-[var(--zui-copy-button-glass-bg-dark,#ffffff1a)] text-[color:var(--zui-copy-button-glass-fg,#0f172a)] dark:text-[color:var(--zui-copy-button-glass-fg-dark,#ffffff)] backdrop-blur-md hover:bg-[var(--zui-copy-button-glass-bg-hover,#00000026)] dark:hover:bg-[var(--zui-copy-button-glass-bg-hover-dark,#ffffff26)]",
23
+ emerald:
24
+ "bg-[var(--zui-copy-button-emerald-bg,#10b981)] dark:bg-[var(--zui-copy-button-emerald-bg-dark,#065f46)] text-[color:var(--zui-copy-button-emerald-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-emerald-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-emerald-bg-hover,#10b981)] dark:hover:bg-[var(--zui-copy-button-emerald-bg-hover-dark,#064e3b)]",
25
+ indigo:
26
+ "bg-[var(--zui-copy-button-indigo-bg,#3730a3)] dark:bg-[var(--zui-copy-button-indigo-bg-dark,#4f46e5)] text-[color:var(--zui-copy-button-indigo-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-indigo-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-indigo-bg-hover,#3730a3)] dark:hover:bg-[var(--zui-copy-button-indigo-bg-hover-dark,#4f46e5)]",
27
+ purple:
28
+ "bg-[var(--zui-copy-button-purple-bg,#6b21a8)] dark:bg-[var(--zui-copy-button-purple-bg-dark,#9333ea)] text-[color:var(--zui-copy-button-purple-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-purple-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-purple-bg-hover,#6b21a8)] dark:hover:bg-[var(--zui-copy-button-purple-bg-hover-dark,#9333ea)]",
29
+ pink:
30
+ "bg-[var(--zui-copy-button-pink-bg,#9d174d)] dark:bg-[var(--zui-copy-button-pink-bg-dark,#db2777)] text-[color:var(--zui-copy-button-pink-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-pink-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-pink-bg-hover,#9d174d)] dark:hover:bg-[var(--zui-copy-button-pink-bg-hover-dark,#db2777)]",
31
+ rose:
32
+ "bg-[var(--zui-copy-button-rose-bg,#9f1239)] dark:bg-[var(--zui-copy-button-rose-bg-dark,#e11d48)] text-[color:var(--zui-copy-button-rose-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-rose-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-rose-bg-hover,#9f1239)] dark:hover:bg-[var(--zui-copy-button-rose-bg-hover-dark,#e11d48)]",
33
+ sky:
34
+ "bg-[var(--zui-copy-button-sky-bg,#0ea5e9)] dark:bg-[var(--zui-copy-button-sky-bg-dark,#0369a1)] text-[color:var(--zui-copy-button-sky-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-sky-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-sky-bg-hover,#0ea5e9)] dark:hover:bg-[var(--zui-copy-button-sky-bg-hover-dark,#075985)]",
35
+ teal:
36
+ "bg-[var(--zui-copy-button-teal-bg,#14b8a6)] dark:bg-[var(--zui-copy-button-teal-bg-dark,#0f766e)] text-[color:var(--zui-copy-button-teal-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-teal-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-teal-bg-hover,#14b8a6)] dark:hover:bg-[var(--zui-copy-button-teal-bg-hover-dark,#115e59)]",
37
+ yellow:
38
+ "bg-[var(--zui-copy-button-yellow-bg,#eab308)] dark:bg-[var(--zui-copy-button-yellow-bg-dark,#854d0e)] text-[color:var(--zui-copy-button-yellow-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-yellow-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-yellow-bg-hover,#eab308)] dark:hover:bg-[var(--zui-copy-button-yellow-bg-hover-dark,#713f12)]",
39
+ orange:
40
+ "bg-[var(--zui-copy-button-orange-bg,#f97316)] dark:bg-[var(--zui-copy-button-orange-bg-dark,#9a3412)] text-[color:var(--zui-copy-button-orange-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-orange-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-orange-bg-hover,#f97316)] dark:hover:bg-[var(--zui-copy-button-orange-bg-hover-dark,#7c2d12)]",
41
+ gray:
42
+ "bg-[var(--zui-copy-button-gray-bg,#6b7280)] dark:bg-[var(--zui-copy-button-gray-bg-dark,#374151)] text-[color:var(--zui-copy-button-gray-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gray-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-gray-bg-hover,#6b7280)] dark:hover:bg-[var(--zui-copy-button-gray-bg-hover-dark,#1f2937)]",
43
+ amber:
44
+ "bg-[var(--zui-copy-button-amber-bg,#f59e0b)] dark:bg-[var(--zui-copy-button-amber-bg-dark,#92400e)] text-[color:var(--zui-copy-button-amber-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-amber-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-amber-bg-hover,#f59e0b)] dark:hover:bg-[var(--zui-copy-button-amber-bg-hover-dark,#78350f)]",
45
+ violet:
46
+ "bg-[var(--zui-copy-button-violet-bg,#5b21b6)] dark:bg-[var(--zui-copy-button-violet-bg-dark,#7c3aed)] text-[color:var(--zui-copy-button-violet-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-violet-fg-dark,#ffffff)] hover:bg-[var(--zui-copy-button-violet-bg-hover,#5b21b6)] dark:hover:bg-[var(--zui-copy-button-violet-bg-hover-dark,#7c3aed)]",
47
+ "gradient-blue":
48
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-blue-from,#1e40af)] dark:from-[var(--zui-copy-button-gradient-blue-from-dark,#2563eb)] to-[var(--zui-copy-button-gradient-blue-to,#6b21a8)] dark:to-[var(--zui-copy-button-gradient-blue-to-dark,#9333ea)] text-[color:var(--zui-copy-button-gradient-blue-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-blue-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-blue-from-hover,#1e40af)] dark:hover:from-[var(--zui-copy-button-gradient-blue-from-hover-dark,#2563eb)] hover:to-[var(--zui-copy-button-gradient-blue-to-hover,#6b21a8)] dark:hover:to-[var(--zui-copy-button-gradient-blue-to-hover-dark,#9333ea)]",
49
+ "gradient-green":
50
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-green-from,#166534)] dark:from-[var(--zui-copy-button-gradient-green-from-dark,#16a34a)] to-[var(--zui-copy-button-gradient-green-to,#3f6212)] dark:to-[var(--zui-copy-button-gradient-green-to-dark,#65a30d)] text-[color:var(--zui-copy-button-gradient-green-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-green-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-green-from-hover,#166534)] dark:hover:from-[var(--zui-copy-button-gradient-green-from-hover-dark,#16a34a)] hover:to-[var(--zui-copy-button-gradient-green-to-hover,#3f6212)] dark:hover:to-[var(--zui-copy-button-gradient-green-to-hover-dark,#65a30d)]",
51
+ "gradient-red":
52
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-red-from,#991b1b)] dark:from-[var(--zui-copy-button-gradient-red-from-dark,#dc2626)] to-[var(--zui-copy-button-gradient-red-to,#9d174d)] dark:to-[var(--zui-copy-button-gradient-red-to-dark,#db2777)] text-[color:var(--zui-copy-button-gradient-red-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-red-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-red-from-hover,#991b1b)] dark:hover:from-[var(--zui-copy-button-gradient-red-from-hover-dark,#dc2626)] hover:to-[var(--zui-copy-button-gradient-red-to-hover,#9d174d)] dark:hover:to-[var(--zui-copy-button-gradient-red-to-hover-dark,#db2777)]",
53
+ "gradient-yellow":
54
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-yellow-from,#854d0e)] dark:from-[var(--zui-copy-button-gradient-yellow-from-dark,#ca8a04)] to-[var(--zui-copy-button-gradient-yellow-to,#9a3412)] dark:to-[var(--zui-copy-button-gradient-yellow-to-dark,#ea580c)] text-[color:var(--zui-copy-button-gradient-yellow-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-yellow-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-yellow-from-hover,#854d0e)] dark:hover:from-[var(--zui-copy-button-gradient-yellow-from-hover-dark,#ca8a04)] hover:to-[var(--zui-copy-button-gradient-yellow-to-hover,#9a3412)] dark:hover:to-[var(--zui-copy-button-gradient-yellow-to-hover-dark,#ea580c)]",
55
+ "gradient-purple":
56
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-purple-from,#6b21a8)] dark:from-[var(--zui-copy-button-gradient-purple-from-dark,#9333ea)] to-[var(--zui-copy-button-gradient-purple-to,#9d174d)] dark:to-[var(--zui-copy-button-gradient-purple-to-dark,#db2777)] text-[color:var(--zui-copy-button-gradient-purple-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-purple-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-purple-from-hover,#6b21a8)] dark:hover:from-[var(--zui-copy-button-gradient-purple-from-hover-dark,#9333ea)] hover:to-[var(--zui-copy-button-gradient-purple-to-hover,#9d174d)] dark:hover:to-[var(--zui-copy-button-gradient-purple-to-hover-dark,#db2777)]",
57
+ "gradient-teal":
58
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-teal-from,#115e59)] dark:from-[var(--zui-copy-button-gradient-teal-from-dark,#0d9488)] to-[var(--zui-copy-button-gradient-teal-to,#155e75)] dark:to-[var(--zui-copy-button-gradient-teal-to-dark,#0891b2)] text-[color:var(--zui-copy-button-gradient-teal-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-teal-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-teal-from-hover,#115e59)] dark:hover:from-[var(--zui-copy-button-gradient-teal-from-hover-dark,#0d9488)] hover:to-[var(--zui-copy-button-gradient-teal-to-hover,#155e75)] dark:hover:to-[var(--zui-copy-button-gradient-teal-to-hover-dark,#0891b2)]",
59
+ "gradient-indigo":
60
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-indigo-from,#3730a3)] dark:from-[var(--zui-copy-button-gradient-indigo-from-dark,#4f46e5)] to-[var(--zui-copy-button-gradient-indigo-to,#6b21a8)] dark:to-[var(--zui-copy-button-gradient-indigo-to-dark,#9333ea)] text-[color:var(--zui-copy-button-gradient-indigo-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-indigo-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-indigo-from-hover,#3730a3)] dark:hover:from-[var(--zui-copy-button-gradient-indigo-from-hover-dark,#4f46e5)] hover:to-[var(--zui-copy-button-gradient-indigo-to-hover,#6b21a8)] dark:hover:to-[var(--zui-copy-button-gradient-indigo-to-hover-dark,#9333ea)]",
61
+ "gradient-pink":
62
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-pink-from,#9d174d)] dark:from-[var(--zui-copy-button-gradient-pink-from-dark,#db2777)] to-[var(--zui-copy-button-gradient-pink-to,#9f1239)] dark:to-[var(--zui-copy-button-gradient-pink-to-dark,#e11d48)] text-[color:var(--zui-copy-button-gradient-pink-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-pink-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-pink-from-hover,#9d174d)] dark:hover:from-[var(--zui-copy-button-gradient-pink-from-hover-dark,#db2777)] hover:to-[var(--zui-copy-button-gradient-pink-to-hover,#9f1239)] dark:hover:to-[var(--zui-copy-button-gradient-pink-to-hover-dark,#e11d48)]",
63
+ "gradient-orange":
64
+ "bg-linear-to-r from-[var(--zui-copy-button-gradient-orange-from,#9a3412)] dark:from-[var(--zui-copy-button-gradient-orange-from-dark,#ea580c)] to-[var(--zui-copy-button-gradient-orange-to,#991b1b)] dark:to-[var(--zui-copy-button-gradient-orange-to-dark,#dc2626)] text-[color:var(--zui-copy-button-gradient-orange-fg,#ffffff)] dark:text-[color:var(--zui-copy-button-gradient-orange-fg-dark,#ffffff)] hover:from-[var(--zui-copy-button-gradient-orange-from-hover,#9a3412)] dark:hover:from-[var(--zui-copy-button-gradient-orange-from-hover-dark,#ea580c)] hover:to-[var(--zui-copy-button-gradient-orange-to-hover,#991b1b)] dark:hover:to-[var(--zui-copy-button-gradient-orange-to-hover-dark,#dc2626)]",
65
+ } as const;
66
+
67
+ export type ZuiCopyButtonAppearance = keyof typeof zuiCopyButtonAppearances;
68
+
69
+ export const zuiCopyButtonSizes = {
70
+ sm: "h-8 px-2.5 text-xs [&_svg]:size-3.5",
71
+ md: "h-9 px-3 text-sm [&_svg]:size-4",
72
+ lg: "h-11 px-4 text-base [&_svg]:size-5",
73
+ } as const;
74
+
75
+ export type ZuiCopyButtonSize = keyof typeof zuiCopyButtonSizes;
76
+
77
+ export const zuiCopyButtonIconOnlySizes = {
78
+ sm: "w-8 px-0",
79
+ md: "w-9 px-0",
80
+ lg: "w-11 px-0",
81
+ } as const;
@@ -9,6 +9,7 @@ export * from "./card";
9
9
  export * from "./checkbox";
10
10
  export * from "./command";
11
11
  export * from "./context-menu";
12
+ export * from "./copy-button";
12
13
  export * from "./divider";
13
14
  export * from "./drawer";
14
15
  export * from "./dropdown";
@@ -16,6 +17,7 @@ export * from "./dynamic-stepper";
16
17
  export * from "./empty-state";
17
18
  export * from "./file-upload";
18
19
  export * from "./inputs";
20
+ export * from "./kbd";
19
21
  export * from "./marquee";
20
22
  export * from "./modal";
21
23
  export * from "./otp-input";
@@ -0,0 +1,83 @@
1
+ export const zuiKbdBase = [
2
+ "inline-flex items-center justify-center gap-1 align-middle",
3
+ "rounded-[var(--zui-kbd-radius,0.375rem)] font-mono font-medium leading-none select-none",
4
+ ] as const;
5
+
6
+ export const zuiKbdKeyAppearances = {
7
+ default:
8
+ "bg-[var(--zui-kbd-default-bg,#0f172a)] dark:bg-[var(--zui-kbd-default-bg-dark,#f8fafc)] text-[color:var(--zui-kbd-default-fg,#f8fafc)] dark:text-[color:var(--zui-kbd-default-fg-dark,#020617)] shadow-[var(--zui-kbd-default-shadow,0_1px_2px_#0f172a14)] dark:shadow-[var(--zui-kbd-default-shadow-dark,0_1px_2px_#0f172a1f)]",
9
+ secondary:
10
+ "bg-[var(--zui-kbd-secondary-bg,#e2e8f0)] dark:bg-[var(--zui-kbd-secondary-bg-dark,#1e293b)] text-[color:var(--zui-kbd-secondary-fg,#0f172a)] dark:text-[color:var(--zui-kbd-secondary-fg-dark,#f8fafc)]",
11
+ destructive:
12
+ "bg-[var(--zui-kbd-destructive-bg,#f43f5e)] dark:bg-[var(--zui-kbd-destructive-bg-dark,#be123c)] text-[color:var(--zui-kbd-destructive-fg,#ffffff)] dark:text-[color:var(--zui-kbd-destructive-fg-dark,#ffffff)]",
13
+ outline:
14
+ "border border-[color:var(--zui-kbd-outline-border,#0000001a)] dark:border-[color:var(--zui-kbd-outline-border-dark,#ffffff1a)] bg-[var(--zui-kbd-outline-bg,#0000000d)] dark:bg-[var(--zui-kbd-outline-bg-dark,#ffffff0d)] text-[color:var(--zui-kbd-outline-fg,#0f172a)] dark:text-[color:var(--zui-kbd-outline-fg-dark,#f8fafc)]",
15
+ ghost:
16
+ "bg-transparent text-[color:var(--zui-kbd-ghost-fg,#334155)] dark:text-[color:var(--zui-kbd-ghost-fg-dark,#e2e8f0)]",
17
+ glass:
18
+ "border border-[color:var(--zui-kbd-glass-border,#00000026)] dark:border-[color:var(--zui-kbd-glass-border-dark,#ffffff26)] bg-[var(--zui-kbd-glass-bg,#0000001a)] dark:bg-[var(--zui-kbd-glass-bg-dark,#ffffff1a)] text-[color:var(--zui-kbd-glass-fg,#0f172a)] dark:text-[color:var(--zui-kbd-glass-fg-dark,#ffffff)] backdrop-blur-md",
19
+ emerald:
20
+ "bg-[var(--zui-kbd-emerald-bg,#10b981)] dark:bg-[var(--zui-kbd-emerald-bg-dark,#065f46)] text-[color:var(--zui-kbd-emerald-fg,#ffffff)] dark:text-[color:var(--zui-kbd-emerald-fg-dark,#ffffff)]",
21
+ indigo:
22
+ "bg-[var(--zui-kbd-indigo-bg,#3730a3)] dark:bg-[var(--zui-kbd-indigo-bg-dark,#4f46e5)] text-[color:var(--zui-kbd-indigo-fg,#ffffff)] dark:text-[color:var(--zui-kbd-indigo-fg-dark,#ffffff)]",
23
+ purple:
24
+ "bg-[var(--zui-kbd-purple-bg,#6b21a8)] dark:bg-[var(--zui-kbd-purple-bg-dark,#9333ea)] text-[color:var(--zui-kbd-purple-fg,#ffffff)] dark:text-[color:var(--zui-kbd-purple-fg-dark,#ffffff)]",
25
+ pink:
26
+ "bg-[var(--zui-kbd-pink-bg,#9d174d)] dark:bg-[var(--zui-kbd-pink-bg-dark,#db2777)] text-[color:var(--zui-kbd-pink-fg,#ffffff)] dark:text-[color:var(--zui-kbd-pink-fg-dark,#ffffff)]",
27
+ rose:
28
+ "bg-[var(--zui-kbd-rose-bg,#9f1239)] dark:bg-[var(--zui-kbd-rose-bg-dark,#e11d48)] text-[color:var(--zui-kbd-rose-fg,#ffffff)] dark:text-[color:var(--zui-kbd-rose-fg-dark,#ffffff)]",
29
+ sky:
30
+ "bg-[var(--zui-kbd-sky-bg,#0ea5e9)] dark:bg-[var(--zui-kbd-sky-bg-dark,#0369a1)] text-[color:var(--zui-kbd-sky-fg,#ffffff)] dark:text-[color:var(--zui-kbd-sky-fg-dark,#ffffff)]",
31
+ teal:
32
+ "bg-[var(--zui-kbd-teal-bg,#14b8a6)] dark:bg-[var(--zui-kbd-teal-bg-dark,#0f766e)] text-[color:var(--zui-kbd-teal-fg,#ffffff)] dark:text-[color:var(--zui-kbd-teal-fg-dark,#ffffff)]",
33
+ yellow:
34
+ "bg-[var(--zui-kbd-yellow-bg,#eab308)] dark:bg-[var(--zui-kbd-yellow-bg-dark,#854d0e)] text-[color:var(--zui-kbd-yellow-fg,#ffffff)] dark:text-[color:var(--zui-kbd-yellow-fg-dark,#ffffff)]",
35
+ orange:
36
+ "bg-[var(--zui-kbd-orange-bg,#f97316)] dark:bg-[var(--zui-kbd-orange-bg-dark,#9a3412)] text-[color:var(--zui-kbd-orange-fg,#ffffff)] dark:text-[color:var(--zui-kbd-orange-fg-dark,#ffffff)]",
37
+ gray:
38
+ "bg-[var(--zui-kbd-gray-bg,#6b7280)] dark:bg-[var(--zui-kbd-gray-bg-dark,#374151)] text-[color:var(--zui-kbd-gray-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gray-fg-dark,#ffffff)]",
39
+ amber:
40
+ "bg-[var(--zui-kbd-amber-bg,#f59e0b)] dark:bg-[var(--zui-kbd-amber-bg-dark,#92400e)] text-[color:var(--zui-kbd-amber-fg,#ffffff)] dark:text-[color:var(--zui-kbd-amber-fg-dark,#ffffff)]",
41
+ violet:
42
+ "bg-[var(--zui-kbd-violet-bg,#5b21b6)] dark:bg-[var(--zui-kbd-violet-bg-dark,#7c3aed)] text-[color:var(--zui-kbd-violet-fg,#ffffff)] dark:text-[color:var(--zui-kbd-violet-fg-dark,#ffffff)]",
43
+ "gradient-blue":
44
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-blue-from,#1e40af)] dark:from-[var(--zui-kbd-gradient-blue-from-dark,#2563eb)] to-[var(--zui-kbd-gradient-blue-to,#6b21a8)] dark:to-[var(--zui-kbd-gradient-blue-to-dark,#9333ea)] text-[color:var(--zui-kbd-gradient-blue-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-blue-fg-dark,#ffffff)]",
45
+ "gradient-green":
46
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-green-from,#166534)] dark:from-[var(--zui-kbd-gradient-green-from-dark,#16a34a)] to-[var(--zui-kbd-gradient-green-to,#3f6212)] dark:to-[var(--zui-kbd-gradient-green-to-dark,#65a30d)] text-[color:var(--zui-kbd-gradient-green-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-green-fg-dark,#ffffff)]",
47
+ "gradient-red":
48
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-red-from,#991b1b)] dark:from-[var(--zui-kbd-gradient-red-from-dark,#dc2626)] to-[var(--zui-kbd-gradient-red-to,#9d174d)] dark:to-[var(--zui-kbd-gradient-red-to-dark,#db2777)] text-[color:var(--zui-kbd-gradient-red-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-red-fg-dark,#ffffff)]",
49
+ "gradient-yellow":
50
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-yellow-from,#854d0e)] dark:from-[var(--zui-kbd-gradient-yellow-from-dark,#ca8a04)] to-[var(--zui-kbd-gradient-yellow-to,#9a3412)] dark:to-[var(--zui-kbd-gradient-yellow-to-dark,#ea580c)] text-[color:var(--zui-kbd-gradient-yellow-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-yellow-fg-dark,#ffffff)]",
51
+ "gradient-purple":
52
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-purple-from,#6b21a8)] dark:from-[var(--zui-kbd-gradient-purple-from-dark,#9333ea)] to-[var(--zui-kbd-gradient-purple-to,#9d174d)] dark:to-[var(--zui-kbd-gradient-purple-to-dark,#db2777)] text-[color:var(--zui-kbd-gradient-purple-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-purple-fg-dark,#ffffff)]",
53
+ "gradient-teal":
54
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-teal-from,#115e59)] dark:from-[var(--zui-kbd-gradient-teal-from-dark,#0d9488)] to-[var(--zui-kbd-gradient-teal-to,#155e75)] dark:to-[var(--zui-kbd-gradient-teal-to-dark,#0891b2)] text-[color:var(--zui-kbd-gradient-teal-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-teal-fg-dark,#ffffff)]",
55
+ "gradient-indigo":
56
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-indigo-from,#3730a3)] dark:from-[var(--zui-kbd-gradient-indigo-from-dark,#4f46e5)] to-[var(--zui-kbd-gradient-indigo-to,#6b21a8)] dark:to-[var(--zui-kbd-gradient-indigo-to-dark,#9333ea)] text-[color:var(--zui-kbd-gradient-indigo-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-indigo-fg-dark,#ffffff)]",
57
+ "gradient-pink":
58
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-pink-from,#9d174d)] dark:from-[var(--zui-kbd-gradient-pink-from-dark,#db2777)] to-[var(--zui-kbd-gradient-pink-to,#9f1239)] dark:to-[var(--zui-kbd-gradient-pink-to-dark,#e11d48)] text-[color:var(--zui-kbd-gradient-pink-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-pink-fg-dark,#ffffff)]",
59
+ "gradient-orange":
60
+ "bg-linear-to-r from-[var(--zui-kbd-gradient-orange-from,#9a3412)] dark:from-[var(--zui-kbd-gradient-orange-from-dark,#ea580c)] to-[var(--zui-kbd-gradient-orange-to,#991b1b)] dark:to-[var(--zui-kbd-gradient-orange-to-dark,#dc2626)] text-[color:var(--zui-kbd-gradient-orange-fg,#ffffff)] dark:text-[color:var(--zui-kbd-gradient-orange-fg-dark,#ffffff)]",
61
+ } as const;
62
+
63
+ export type ZuiKbdAppearance = keyof typeof zuiKbdKeyAppearances;
64
+
65
+ export const zuiKbdKeyBase = [
66
+ "inline-flex items-center justify-center font-mono font-medium leading-none",
67
+ "rounded-[var(--zui-kbd-radius,0.375rem)]",
68
+ "shadow-[var(--zui-kbd-shadow,inset_0_-1px_0_#0000001f)] dark:shadow-[var(--zui-kbd-shadow-dark,inset_0_-1px_0_#0000004d)]",
69
+ ] as const;
70
+
71
+ export const zuiKbdKeySizes = {
72
+ sm: "h-5 min-w-5 px-1 text-[0.7rem]",
73
+ md: "h-6 min-w-6 px-1.5 text-xs",
74
+ lg: "h-7 min-w-7 px-2 text-sm",
75
+ } as const;
76
+
77
+ export type ZuiKbdSize = keyof typeof zuiKbdKeySizes;
78
+
79
+ export const zuiKbdSeparatorSizes = {
80
+ sm: "text-[0.7rem]",
81
+ md: "text-xs",
82
+ lg: "text-sm",
83
+ } as const;
@@ -0,0 +1,22 @@
1
+ import type { CopyButtonAnimationPresets } from "./types";
2
+
3
+ export const copyButtonAnimationPresets: CopyButtonAnimationPresets = {
4
+ swap: {
5
+ initial: { opacity: 0, scale: 0.6, rotate: -45 },
6
+ animate: { opacity: 1, scale: 1, rotate: 0 },
7
+ exit: { opacity: 0, scale: 0.6, rotate: 45 },
8
+ transition: { type: "spring", stiffness: 520, damping: 24 },
9
+ },
10
+ pop: {
11
+ initial: { opacity: 0, scale: 0.4, rotate: 0 },
12
+ animate: { opacity: 1, scale: 1, rotate: 0 },
13
+ exit: { opacity: 0, scale: 0.4, rotate: 0 },
14
+ transition: { type: "spring", stiffness: 600, damping: 20 },
15
+ },
16
+ fade: {
17
+ initial: { opacity: 0, scale: 1, rotate: 0 },
18
+ animate: { opacity: 1, scale: 1, rotate: 0 },
19
+ exit: { opacity: 0, scale: 1, rotate: 0 },
20
+ transition: { duration: 0.16 },
21
+ },
22
+ };
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { AnimatePresence, motion } from "framer-motion";
4
+
5
+ import { CopyButtonBase } from "../copy-button-base";
6
+ import type { CopyButtonIconRenderer } from "../types";
7
+
8
+ import { copyButtonAnimationPresets } from "./animations";
9
+ import type { CopyButtonAnimatedProps } from "./types";
10
+
11
+ export function CopyButtonAnimated({
12
+ animation = "swap",
13
+ ...props
14
+ }: CopyButtonAnimatedProps) {
15
+ const preset = copyButtonAnimationPresets[animation];
16
+
17
+ const renderIcon: CopyButtonIconRenderer = ({
18
+ copied,
19
+ copyIcon,
20
+ copiedIcon,
21
+ }) => (
22
+ <AnimatePresence initial={false} mode="wait">
23
+ <motion.span
24
+ key={copied ? "copied" : "idle"}
25
+ className="inline-flex items-center justify-center"
26
+ initial={preset.initial}
27
+ animate={preset.animate}
28
+ exit={preset.exit}
29
+ transition={preset.transition}
30
+ >
31
+ {copied ? copiedIcon : copyIcon}
32
+ </motion.span>
33
+ </AnimatePresence>
34
+ );
35
+
36
+ return <CopyButtonBase {...props} renderIcon={renderIcon} />;
37
+ }
38
+
39
+ CopyButtonAnimated.displayName = "CopyButtonAnimated";
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export { CopyButtonAnimated } from "./copy-button-animated";
4
+ export { copyButtonAnimationPresets } from "./animations";
5
+ export type {
6
+ CopyButtonAnimatedProps,
7
+ CopyButtonAnimation,
8
+ CopyButtonAnimationPreset,
9
+ CopyButtonAnimationPresets,
10
+ } from "./types";
@@ -0,0 +1,21 @@
1
+ import type { Transition } from "framer-motion";
2
+
3
+ import type { CopyButtonProps } from "../types";
4
+
5
+ export type CopyButtonAnimation = "swap" | "pop" | "fade";
6
+
7
+ export type CopyButtonAnimatedProps = CopyButtonProps & {
8
+ animation?: CopyButtonAnimation;
9
+ };
10
+
11
+ export type CopyButtonAnimationPreset = {
12
+ initial: { opacity: number; scale: number; rotate: number };
13
+ animate: { opacity: number; scale: number; rotate: number };
14
+ exit: { opacity: number; scale: number; rotate: number };
15
+ transition: Transition;
16
+ };
17
+
18
+ export type CopyButtonAnimationPresets = Record<
19
+ CopyButtonAnimation,
20
+ CopyButtonAnimationPreset
21
+ >;
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import { FiCheck, FiCopy } from "react-icons/fi";
4
+
5
+ import { useClipboard } from "../../hooks/useClipboard/useClipboard";
6
+ import { cn } from "../../lib/utils";
7
+
8
+ import type { CopyButtonBaseProps, CopyButtonIconRenderer } from "./types";
9
+ import { copyButtonVariants } from "./variants";
10
+
11
+ const defaultRenderIcon: CopyButtonIconRenderer = ({
12
+ copied,
13
+ copyIcon,
14
+ copiedIcon,
15
+ }) => (copied ? copiedIcon : copyIcon);
16
+
17
+ export function CopyButtonBase({
18
+ value,
19
+ timeout = 2000,
20
+ appearance,
21
+ size,
22
+ iconOnly = true,
23
+ label = "Copy",
24
+ copiedLabel = "Copied",
25
+ copyIcon = <FiCopy aria-hidden />,
26
+ copiedIcon = <FiCheck aria-hidden />,
27
+ onCopy,
28
+ renderIcon = defaultRenderIcon,
29
+ className,
30
+ type = "button",
31
+ disabled,
32
+ onClick,
33
+ "aria-label": ariaLabel,
34
+ ref,
35
+ ...rest
36
+ }: CopyButtonBaseProps) {
37
+ const { copied, copy } = useClipboard(timeout);
38
+
39
+ const handleClick: NonNullable<CopyButtonBaseProps["onClick"]> = async (
40
+ event,
41
+ ) => {
42
+ onClick?.(event);
43
+ if (event.defaultPrevented) {
44
+ return;
45
+ }
46
+ const ok = await copy(value);
47
+ if (ok) {
48
+ onCopy?.(value);
49
+ }
50
+ };
51
+
52
+ const text = copied ? copiedLabel : label;
53
+
54
+ return (
55
+ <button
56
+ ref={ref}
57
+ type={type}
58
+ data-slot="copy-button"
59
+ data-copied={copied ? "true" : undefined}
60
+ disabled={disabled}
61
+ aria-label={ariaLabel ?? (iconOnly ? text : undefined)}
62
+ onClick={handleClick}
63
+ className={cn(
64
+ copyButtonVariants({ appearance, size, iconOnly }),
65
+ className,
66
+ )}
67
+ {...rest}
68
+ >
69
+ <span
70
+ data-slot="copy-button-icon"
71
+ className="relative inline-flex items-center justify-center"
72
+ >
73
+ {renderIcon({ copied, copyIcon, copiedIcon })}
74
+ </span>
75
+ {!iconOnly ? (
76
+ <span data-slot="copy-button-label" aria-live="polite">
77
+ {text}
78
+ </span>
79
+ ) : (
80
+ <span className="sr-only" aria-live="polite">
81
+ {text}
82
+ </span>
83
+ )}
84
+ </button>
85
+ );
86
+ }
87
+
88
+ CopyButtonBase.displayName = "CopyButton";
@@ -0,0 +1,82 @@
1
+ import { createRef } from "react";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { CopyButton } from "./copy-button";
7
+
8
+ describe("CopyButton", () => {
9
+ const originalClipboard = navigator.clipboard;
10
+
11
+ beforeEach(() => {
12
+ Object.defineProperty(navigator, "clipboard", {
13
+ configurable: true,
14
+ writable: true,
15
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
16
+ });
17
+ });
18
+
19
+ afterEach(() => {
20
+ Object.defineProperty(navigator, "clipboard", {
21
+ configurable: true,
22
+ writable: true,
23
+ value: originalClipboard,
24
+ });
25
+ });
26
+
27
+ it("should expose displayName", () => {
28
+ expect(CopyButton.displayName).toBe("CopyButton");
29
+ });
30
+
31
+ it("should stamp data-slot", () => {
32
+ render(<CopyButton value="npm i zentauri" />);
33
+ expect(
34
+ document.querySelector('[data-slot="copy-button"]'),
35
+ ).toBeInTheDocument();
36
+ });
37
+
38
+ it("should write the value to the clipboard on click", async () => {
39
+ render(<CopyButton value="copy me" />);
40
+ fireEvent.click(screen.getByRole("button"));
41
+ await waitFor(() =>
42
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith("copy me"),
43
+ );
44
+ });
45
+
46
+ it("should call onCopy after a successful copy", async () => {
47
+ const user = userEvent.setup();
48
+ const onCopy = vi.fn();
49
+ render(<CopyButton value="token-123" onCopy={onCopy} />);
50
+ await user.click(screen.getByRole("button"));
51
+ await waitFor(() => expect(onCopy).toHaveBeenCalledWith("token-123"));
52
+ });
53
+
54
+ it("should flip to the copied state and mark data-copied", async () => {
55
+ const user = userEvent.setup();
56
+ render(<CopyButton value="x" timeout={0} copiedLabel="Copied!" />);
57
+ const button = screen.getByRole("button");
58
+ await user.click(button);
59
+ await waitFor(() =>
60
+ expect(button.getAttribute("data-copied")).toBe("true"),
61
+ );
62
+ });
63
+
64
+ it("should render the label when iconOnly is false", () => {
65
+ render(<CopyButton value="x" iconOnly={false} label="Copy code" />);
66
+ expect(screen.getByText("Copy code")).toBeInTheDocument();
67
+ });
68
+
69
+ it("should apply the secondary appearance token", () => {
70
+ render(<CopyButton value="x" appearance="secondary" />);
71
+ const button = document.querySelector(
72
+ '[data-slot="copy-button"]',
73
+ ) as HTMLElement;
74
+ expect(button.className).toMatch(/--zui-copy-button-secondary-bg/);
75
+ });
76
+
77
+ it("should forward ref", () => {
78
+ const ref = createRef<HTMLButtonElement>();
79
+ render(<CopyButton ref={ref} value="x" />);
80
+ expect(ref.current?.getAttribute("data-slot")).toBe("copy-button");
81
+ });
82
+ });
@@ -0,0 +1,9 @@
1
+ // copy-button.tsx — default static entry (no framer-motion)
2
+ import { CopyButtonBase } from "./copy-button-base";
3
+ import type { CopyButtonProps } from "./types";
4
+
5
+ export function CopyButton(props: CopyButtonProps) {
6
+ return <CopyButtonBase {...props} />;
7
+ }
8
+
9
+ CopyButton.displayName = "CopyButton";
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export { CopyButton } from "./copy-button";
4
+ export type {
5
+ CopyButtonBaseProps,
6
+ CopyButtonIconRenderer,
7
+ CopyButtonProps,
8
+ CopyButtonVariantProps,
9
+ } from "./types";
10
+ export { copyButtonVariants } from "./variants";
@@ -0,0 +1,37 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+
4
+ import type { copyButtonVariants } from "./variants";
5
+
6
+ export type CopyButtonVariantProps = VariantProps<typeof copyButtonVariants>;
7
+
8
+ /** Renders the icon region for a given copied state. Lets the animated entry swap the static icons for motion ones. */
9
+ export type CopyButtonIconRenderer = (state: {
10
+ copied: boolean;
11
+ copyIcon: ReactNode;
12
+ copiedIcon: ReactNode;
13
+ }) => ReactNode;
14
+
15
+ export interface CopyButtonBaseProps
16
+ extends Omit<ComponentPropsWithRef<"button">, "value" | "onCopy"> {
17
+ /** Text written to the clipboard when the button is pressed. */
18
+ value: string;
19
+ /** Milliseconds the copied state stays active before resetting. `0` keeps it until re-copied. */
20
+ timeout?: number;
21
+ appearance?: CopyButtonVariantProps["appearance"];
22
+ size?: CopyButtonVariantProps["size"];
23
+ /** Render only the icon (default). Pass `false` to show the label text alongside the icon. */
24
+ iconOnly?: boolean;
25
+ /** Label shown (and used for `aria-label`) in the idle state. */
26
+ label?: string;
27
+ /** Label shown (and used for `aria-label`) after a successful copy. */
28
+ copiedLabel?: string;
29
+ copyIcon?: ReactNode;
30
+ copiedIcon?: ReactNode;
31
+ /** Called with `value` after the clipboard write succeeds. */
32
+ onCopy?: (value: string) => void;
33
+ /** Overrides how the icon region renders; the animated entry uses this for motion. */
34
+ renderIcon?: CopyButtonIconRenderer;
35
+ }
36
+
37
+ export type CopyButtonProps = Omit<CopyButtonBaseProps, "renderIcon">;
@@ -0,0 +1,29 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ import {
4
+ zuiCopyButtonAppearances,
5
+ zuiCopyButtonBase,
6
+ zuiCopyButtonIconOnlySizes,
7
+ zuiCopyButtonSizes,
8
+ } from "../../design-system/copy-button";
9
+
10
+ export const copyButtonVariants = cva(zuiCopyButtonBase, {
11
+ variants: {
12
+ appearance: zuiCopyButtonAppearances,
13
+ size: zuiCopyButtonSizes,
14
+ iconOnly: {
15
+ true: "",
16
+ false: "",
17
+ },
18
+ },
19
+ compoundVariants: [
20
+ { iconOnly: true, size: "sm", class: zuiCopyButtonIconOnlySizes.sm },
21
+ { iconOnly: true, size: "md", class: zuiCopyButtonIconOnlySizes.md },
22
+ { iconOnly: true, size: "lg", class: zuiCopyButtonIconOnlySizes.lg },
23
+ ],
24
+ defaultVariants: {
25
+ appearance: "default",
26
+ size: "md",
27
+ iconOnly: true,
28
+ },
29
+ });
@@ -0,0 +1,15 @@
1
+ import type { KbdAnimationPresets } from "./types";
2
+
3
+ export const kbdAnimationPresets: KbdAnimationPresets = {
4
+ none: {},
5
+ press: {
6
+ whileHover: { y: -1 },
7
+ whileTap: { y: 1, scale: 0.96 },
8
+ transition: { type: "spring", stiffness: 600, damping: 22 },
9
+ },
10
+ pop: {
11
+ initial: { scale: 0.85, opacity: 0 },
12
+ animate: { scale: 1, opacity: 1 },
13
+ transition: { type: "spring", stiffness: 520, damping: 26 },
14
+ },
15
+ };
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ export { KbdAnimated } from "./kbd-animated";
4
+ export { kbdAnimationPresets } from "./animations";
5
+ export type {
6
+ KbdAnimatedProps,
7
+ KbdAnimation,
8
+ KbdAnimationPresets,
9
+ } from "./types";