{ "$schema": "https://ui.shadcn.com/schema/registry.json", "name": "duck", "homepage": "https://duckui.davideghiotto.it", "items": [ { "name": "theme", "type": "registry:theme", "title": "Duck Theme", "description": "duck/ui theme: dark-first canvas, duck-lime primary, holographic accents. Installs the full shadcn CSS-variable contract plus the holo utilities, the display scale, the control-typography tokens, the motion vocabulary and a print layer that re-asserts the light palette on paper, so every existing shadcn component is duck-styled with no markup changes — and so a different theme can undo the duck styling without touching any. It declares no typefaces: --font-sans and --font-mono stay Tailwind's, and --font-display follows --font-sans until you point it at a display face.", "cssVars": { "theme": { "radius": "0.75rem", "radius-sm": "calc(var(--radius) * 0.667)", "radius-md": "calc(var(--radius) * 0.833)", "radius-lg": "var(--radius)", "radius-xl": "calc(var(--radius) * 1.333)", "radius-2xl": "calc(var(--radius) * 1.833)", "font-display": "var(--font-sans)", "ease-duck": "cubic-bezier(0.16, 1, 0.3, 1)", "ease-squash": "cubic-bezier(0.34, 1.56, 0.64, 1)", "color-surface": "var(--surface)", "color-surface-raised": "var(--surface-raised)", "color-sheet": "var(--sheet)", "color-sheet-line": "var(--sheet-line)", "color-cut": "var(--cut)", "color-vinyl": "var(--vinyl)" }, "light": { "background": "oklch(0.985 0.004 285)", "foreground": "oklch(0.18 0.012 285)", "card": "oklch(1 0 0)", "card-foreground": "oklch(0.18 0.012 285)", "popover": "oklch(1 0 0)", "popover-foreground": "oklch(0.18 0.012 285)", "primary": "oklch(0.76 0.16 115)", "primary-foreground": "oklch(0.22 0.06 115)", "secondary": "oklch(0.945 0.006 285)", "secondary-foreground": "oklch(0.25 0.012 285)", "muted": "oklch(0.955 0.005 285)", "muted-foreground": "oklch(0.48 0.012 285)", "accent": "oklch(0.93 0.03 200)", "accent-foreground": "oklch(0.25 0.05 220)", "destructive": "oklch(0.58 0.22 27)", "destructive-foreground": "oklch(0.98 0 0)", "border": "oklch(0.895 0.006 285)", "input": "oklch(0.895 0.006 285)", "ring": "oklch(0.62 0.14 115)", "chart-1": "oklch(0.72 0.2 305)", "chart-2": "oklch(0.75 0.14 220)", "chart-3": "oklch(0.78 0.14 160)", "chart-4": "oklch(0.8 0.17 115)", "chart-5": "oklch(0.7 0.19 350)", "holo": "linear-gradient(135deg, oklch(0.72 0.2 305), oklch(0.76 0.15 250), oklch(0.8 0.14 195), oklch(0.85 0.17 150))", "foil": "conic-gradient(from 200deg at 50% 50%, oklch(0.79 0.19 20), oklch(0.85 0.16 72), oklch(0.88 0.18 118), oklch(0.84 0.14 172), oklch(0.78 0.16 236), oklch(0.74 0.21 304), oklch(0.79 0.19 20))", "glow": "0 0 24px oklch(0.78 0.15 195 / 0.2)", "glow-primary": "0 0 24px oklch(0.76 0.16 115 / 0.35)", "sticker-border": "3px", "sheen": "oklch(1 0 0 / 0.16)", "surface": "oklch(1 0 0)", "surface-raised": "oklch(0.975 0.004 285)", "glass": "oklch(1 0 0 / 0.72)", "glass-blur": "12px", "font-button": "var(--font-sans)", "weight-button": "600", "tracking-button": "normal", "case-button": "none", "text-button-sm": "0.75rem", "text-button": "0.875rem", "text-button-lg": "1rem", "font-badge": "var(--font-sans)", "weight-badge": "600", "tracking-badge": "normal", "case-badge": "none", "text-badge": "0.75rem", "vinyl": "oklch(0.975 0.004 285)", "sheet": "oklch(0.97 0.004 285)", "sheet-line": "oklch(0.9 0.006 285)", "cut": "oklch(0.78 0.01 285)", "fx": "50", "fy": "50", "rx": "0deg", "ry": "0deg" }, "dark": { "background": "oklch(0.145 0.006 285)", "foreground": "oklch(0.965 0.004 285)", "card": "oklch(0.185 0.007 285)", "card-foreground": "oklch(0.965 0.004 285)", "popover": "oklch(0.175 0.007 285)", "popover-foreground": "oklch(0.965 0.004 285)", "primary": "oklch(0.85 0.17 115)", "primary-foreground": "oklch(0.24 0.07 115)", "secondary": "oklch(0.235 0.008 285)", "secondary-foreground": "oklch(0.965 0.004 285)", "muted": "oklch(0.225 0.008 285)", "muted-foreground": "oklch(0.68 0.012 285)", "accent": "oklch(0.26 0.03 200)", "accent-foreground": "oklch(0.9 0.05 200)", "destructive": "oklch(0.65 0.2 25)", "destructive-foreground": "oklch(0.98 0 0)", "border": "oklch(0.275 0.009 285)", "input": "oklch(0.275 0.009 285)", "ring": "oklch(0.85 0.17 115)", "chart-1": "oklch(0.72 0.2 305)", "chart-2": "oklch(0.75 0.14 220)", "chart-3": "oklch(0.78 0.14 160)", "chart-4": "oklch(0.85 0.17 115)", "chart-5": "oklch(0.7 0.19 350)", "glow": "0 0 32px oklch(0.78 0.15 195 / 0.25)", "glow-primary": "0 0 32px oklch(0.85 0.17 115 / 0.35)", "surface": "oklch(0.185 0.007 285)", "surface-raised": "oklch(0.225 0.008 285)", "glass": "oklch(0.185 0.007 285 / 0.72)", "sheet": "oklch(0.175 0.006 285)", "sheet-line": "oklch(0.225 0.008 285)", "cut": "oklch(0.34 0.01 285)" } }, "css": { "@layer utilities": { ".holo-border": { "border": "var(--sticker-width, var(--sticker-border)) solid transparent", "background": "linear-gradient(var(--card), var(--card)) padding-box, var(--holo) border-box" }, ".holo-border-animated": { "border": "var(--sticker-width, var(--sticker-border)) solid transparent", "background": "linear-gradient(var(--card), var(--card)) padding-box, var(--holo) border-box", "background-size": "100% 100%, 300% 300%", "animation": "holo-shift 4s ease-in-out infinite" }, ".holo-text": { "background": "var(--holo)", "background-size": "200% 200%", "background-clip": "text", "-webkit-background-clip": "text", "color": "transparent", "animation": "holo-shift 6s ease-in-out infinite" }, ".sticker": { "border-width": "var(--sticker-width, var(--sticker-border))" }, ".sticker-none": { "--sticker-width": "0px" }, ".foil": { "background-image": "radial-gradient(80% 80% at calc(var(--fx) * 1%) calc(var(--fy) * 1%), oklch(1 0 0 / 0.6), oklch(1 0 0 / 0) 60%), var(--foil)", "background-size": "170% 170%, 230% 230%", "background-position": "center, calc(50% + (var(--fx) - 50) * 0.6%) calc(50% + (var(--fy) - 50) * 0.6%)", "background-blend-mode": "overlay, normal" }, ".sheen": { "position": "relative", "overflow": "hidden" }, ".sheen::after": { "content": "\"\"", "position": "absolute", "inset": "0", "pointer-events": "none", "background": "linear-gradient(105deg, transparent 42%, var(--sheen, oklch(1 0 0 / 0.16)) 49%, transparent 56%)", "background-size": "250% 100%", "background-position": "160% 0", "transition": "background-position 0.7s var(--ease-duck)" }, ".sheen:hover::after, .sheen:focus-visible::after": { "background-position": "-40% 0" }, ".tilt": { "transform": "perspective(900px) rotateX(var(--rx)) rotateY(var(--ry))", "transform-style": "preserve-3d", "transition": "transform 0.4s var(--ease-duck)" }, ".tilt-live": { "transition": "transform 0.08s linear" }, ".kiss-cut": { "background-color": "var(--sheet)", "background-image": "repeating-linear-gradient(to right, var(--sheet-line) 0 1px, transparent 1px 100%), repeating-linear-gradient(to bottom, var(--sheet-line) 0 1px, transparent 1px 100%)", "background-size": "28px 28px" }, ".cut-line": { "border": "1.5px dashed var(--cut)" }, ".grain": { "position": "fixed", "inset": "0", "z-index": "60", "pointer-events": "none", "opacity": "0.04", "background-image": "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E\")" }, ".balance": { "text-wrap": "balance" }, ":where(.display-xl)": { "font-family": "var(--font-display)", "font-size": "clamp(2.5rem, 6.5vw, 4.5rem)", "font-weight": "800", "line-height": "0.98", "letter-spacing": "-0.03em" }, ":where(.display-lg)": { "font-family": "var(--font-display)", "font-size": "clamp(2rem, 4.5vw, 3rem)", "font-weight": "700", "line-height": "1.05", "letter-spacing": "-0.025em" }, ":where(.display-md)": { "font-family": "var(--font-display)", "font-size": "clamp(1.5rem, 3vw, 2rem)", "font-weight": "700", "line-height": "1.15", "letter-spacing": "-0.02em" } }, "@layer base": { "*": { "--sticker-width": "initial" }, "[data-slot=\"holo-button\"], [data-slot=\"quack-button\"]": { "font-family": "var(--font-button, var(--font-sans))", "font-size": "var(--text-button, 0.875rem)", "font-weight": "var(--weight-button, 600)", "letter-spacing": "var(--tracking-button, normal)", "text-transform": "var(--case-button, none)" }, "[data-slot=\"holo-button\"][data-size=\"sm\"], [data-slot=\"quack-button\"][data-size=\"sm\"]": { "font-size": "var(--text-button-sm, 0.75rem)" }, "[data-slot=\"holo-button\"][data-size=\"lg\"], [data-slot=\"quack-button\"][data-size=\"lg\"]": { "font-size": "var(--text-button-lg, 1rem)" }, "[data-slot=\"holo-badge\"]": { "font-family": "var(--font-badge, var(--font-sans))", "font-size": "var(--text-badge, 0.75rem)", "font-weight": "var(--weight-badge, 600)", "letter-spacing": "var(--tracking-badge, normal)", "text-transform": "var(--case-badge, none)" }, "[data-slot=\"holo-button\"][data-size=\"xs\"], [data-slot=\"quack-button\"][data-size=\"xs\"], [data-slot=\"holo-button\"][data-size=\"icon-xs\"], [data-slot=\"quack-button\"][data-size=\"icon-xs\"], [data-slot=\"holo-button\"][data-size=\"icon-sm\"], [data-slot=\"quack-button\"][data-size=\"icon-sm\"]": { "font-size": "var(--text-button-sm, 0.75rem)" } }, "@utility duck-glow": { "box-shadow": "var(--glow)" }, "@utility duck-glow-primary": { "box-shadow": "var(--glow-primary)" }, "@utility duck-stream-edge": { "mask-image": "linear-gradient(to bottom, #000 calc(100% - 1.4em), transparent)" }, "@keyframes holo-shift": { "0%, 100%": { "background-position": "0% 0%, 0% 50%" }, "50%": { "background-position": "0% 0%, 100% 50%" } }, "@keyframes duck-waddle": { "0%, 100%": { "transform": "rotate(-12deg) translateY(0)" }, "25%": { "transform": "rotate(0deg) translateY(-2px)" }, "50%": { "transform": "rotate(12deg) translateY(0)" }, "75%": { "transform": "rotate(0deg) translateY(-2px)" } }, "@keyframes duck-idle": { "0%, 90%, 100%": { "transform": "translateY(0) scale(1)" }, "95%": { "transform": "translateY(-1.5px) scale(1.012)" } }, "@keyframes duck-sheen": { "0%, 72%, 100%": { "background-position": "170% 0" }, "88%": { "background-position": "-50% 0" } }, "@keyframes duck-squash": { "0%": { "transform": "scale(1, 1)" }, "40%": { "transform": "scale(1.06, 0.9)" }, "70%": { "transform": "scale(0.97, 1.04)" }, "100%": { "transform": "scale(1, 1)" } }, "@keyframes duck-pop": { "0%": { "transform": "scale(0.6)", "opacity": "0" }, "60%": { "transform": "scale(1.14)", "opacity": "1" }, "100%": { "transform": "scale(1)", "opacity": "1" } }, "@keyframes duck-ripple": { "0%": { "transform": "scale(0)", "opacity": "0.35" }, "100%": { "transform": "scale(2.8)", "opacity": "0" } }, "@keyframes duck-paddle": { "0%, 100%": { "transform": "translateY(0) rotate(-7deg)" }, "50%": { "transform": "translateY(-3px) rotate(7deg)" } }, "@keyframes duck-float": { "0%, 100%": { "transform": "translateY(0) rotate(-1.5deg)" }, "50%": { "transform": "translateY(-9px) rotate(1.5deg)" } }, "@keyframes duck-caret": { "0%, 49%": { "opacity": "1" }, "50%, 100%": { "opacity": "0" } }, "@keyframes duck-marquee": { "from": { "transform": "translateX(0)" }, "to": { "transform": "translateX(-50%)" } }, "@keyframes duck-shimmer": { "from": { "background-position": "-180% 0" }, "to": { "background-position": "180% 0" } }, "@keyframes duck-rise": { "from": { "opacity": "0", "transform": "translateY(14px)" }, "to": { "opacity": "1", "transform": "translateY(0)" } }, "@keyframes duck-fade-in": { "from": { "opacity": "0" }, "to": { "opacity": "1" } }, "@keyframes duck-fade-out": { "from": { "opacity": "1" }, "to": { "opacity": "0" } }, "@keyframes duck-dialog-in": { "from": { "opacity": "0", "transform": "translate(-50%, calc(-50% + 10px)) scale(0.98)" }, "to": { "opacity": "1", "transform": "translate(-50%, -50%) scale(1)" } }, "@keyframes duck-dialog-out": { "from": { "opacity": "1", "transform": "translate(-50%, -50%) scale(1)" }, "to": { "opacity": "0", "transform": "translate(-50%, calc(-50% + 6px)) scale(0.98)" } }, "@media (prefers-reduced-motion: reduce)": { "*, *::before, *::after": { "animation-duration": "0.001ms !important", "animation-iteration-count": "1 !important", "transition-duration": "0.001ms !important", "scroll-behavior": "auto !important" }, ".tilt": { "transform": "none !important" }, ".duck-stream-edge": { "mask-image": "none !important" } }, "@media print": { ":root, .dark": { "--background": "oklch(1 0 0)", "--foreground": "oklch(0.18 0.012 285)", "--card": "oklch(0.985 0.004 285)", "--card-foreground": "oklch(0.18 0.012 285)", "--popover": "oklch(1 0 0)", "--popover-foreground": "oklch(0.18 0.012 285)", "--secondary": "oklch(0.945 0.006 285)", "--secondary-foreground": "oklch(0.25 0.012 285)", "--muted": "oklch(0.955 0.005 285)", "--muted-foreground": "oklch(0.48 0.012 285)", "--accent-foreground": "oklch(0.25 0.05 220)", "--destructive": "oklch(0.58 0.22 27)", "--border": "oklch(0.895 0.006 285)", "--input": "oklch(0.895 0.006 285)", "--primary": "oklch(0.62 0.14 115)", "--primary-foreground": "oklch(1 0 0)", "--ring": "oklch(0.62 0.14 115)", "--glow": "none", "--glow-primary": "none", "--sticker-border": "1px", "--sheet": "oklch(1 0 0)", "--sheet-line": "oklch(0.895 0.006 285)", "--cut": "oklch(0.78 0.01 285)", "--vinyl": "oklch(1 0 0)" }, "html, body": { "background": "var(--background) !important", "color": "var(--foreground) !important" }, ".holo-border, .holo-border-animated, .foil, .sheen": { "background": "none !important", "border-color": "var(--border) !important" }, ".holo-text": { "background": "none !important", "-webkit-text-fill-color": "var(--foreground) !important", "color": "var(--foreground) !important" }, ":where(.duck-prose)": { "max-width": "none" }, ":where(.duck-prose) :is(pre, code, blockquote, mark)": { "background": "none" }, ":where(.duck-prose) :is(figure, blockquote, pre, table, li)": { "break-inside": "avoid" }, ":where(.duck-prose) :is(h2, h3, h4)": { "break-after": "avoid" } } } }, { "name": "theme-noir", "type": "registry:theme", "css": { "@media print": { ":root, .dark": { "--background": "oklch(1 0 0)", "--foreground": "oklch(0.16 0.004 264)", "--card": "oklch(1 0 0)", "--card-foreground": "oklch(0.16 0.004 264)", "--popover": "oklch(1 0 0)", "--popover-foreground": "oklch(0.16 0.004 264)", "--secondary": "oklch(0.96 0.002 264)", "--secondary-foreground": "oklch(0.24 0.004 264)", "--muted": "oklch(0.965 0.002 264)", "--muted-foreground": "oklch(0.46 0.006 264)", "--primary": "oklch(0.52 0.15 135)", "--primary-foreground": "oklch(1 0 0)", "--accent": "oklch(0.52 0.15 135)", "--accent-foreground": "oklch(0.16 0.004 264)", "--border": "oklch(0.86 0.004 264)", "--input": "oklch(0.86 0.004 264)", "--ring": "oklch(0.52 0.15 135)", "--surface": "oklch(1 0 0)", "--surface-raised": "oklch(0.98 0.002 264)", "--glass": "oklch(1 0 0)", "--sheet": "oklch(1 0 0)", "--sheet-line": "oklch(0.86 0.004 264)", "--cut": "oklch(0.72 0.006 264)", "--sheen": "transparent" } } }, "title": "Noir Theme", "description": "Terminal noir: near-black canvas, one lime accent, hairline 1px edges and square corners. Same token contract as the duck theme with the sticker vocabulary dialled to zero — iridescence flattened to lime, glow narrowed, radius collapsed. Proof that a duck/ui install can carry a completely different identity without touching component markup. Unlike the base theme this one does set typefaces — Inter, Space Grotesk and JetBrains Mono are part of the look — so re-declare --font-sans, --font-display and --font-mono after installing it if your project ships its own faces.", "registryDependencies": [ "@duck/theme" ], "cssVars": { "theme": { "radius": "0.125rem", "font-sans": "Inter, system-ui, sans-serif", "font-display": "\"Space Grotesk\", system-ui, sans-serif", "font-mono": "\"JetBrains Mono\", ui-monospace, monospace", "ease-duck": "cubic-bezier(0.16, 1, 0.3, 1)", "ease-squash": "cubic-bezier(0.22, 1, 0.36, 1)" }, "light": { "background": "oklch(0.144 0.003 264)", "foreground": "oklch(0.963 0.003 68)", "card": "oklch(0.18 0.005 253)", "card-foreground": "oklch(0.963 0.003 68)", "popover": "oklch(0.213 0.007 253)", "popover-foreground": "oklch(0.963 0.003 68)", "primary": "oklch(0.897 0.25 135)", "primary-foreground": "oklch(0.144 0.003 264)", "secondary": "oklch(0.235 0.008 253)", "secondary-foreground": "oklch(0.863 0.008 68)", "muted": "oklch(0.224 0.007 253)", "muted-foreground": "oklch(0.703 0.011 68)", "accent": "oklch(0.897 0.25 135)", "accent-foreground": "oklch(0.144 0.003 264)", "destructive": "oklch(0.641 0.218 29)", "destructive-foreground": "oklch(0.963 0.003 68)", "border": "oklch(0.277 0.008 253)", "input": "oklch(0.277 0.008 253)", "ring": "oklch(0.897 0.25 135)", "chart-1": "oklch(0.897 0.25 135)", "chart-2": "oklch(0.75 0.14 220)", "chart-3": "oklch(0.703 0.011 68)", "chart-4": "oklch(0.641 0.218 29)", "chart-5": "oklch(0.863 0.008 68)", "holo": "linear-gradient(135deg, oklch(0.897 0.25 135), oklch(0.897 0.25 135 / 0.3))", "foil": "linear-gradient(135deg, oklch(0.897 0.25 135 / 0.16), transparent 62%)", "glow": "0 0 28px oklch(0.897 0.25 135 / 0.16)", "glow-primary": "0 0 0 1px oklch(0.897 0.25 135 / 0.5), 0 12px 40px -12px oklch(0.897 0.25 135 / 0.6)", "sticker-border": "1px", "sheen": "oklch(0.897 0.25 135 / 0.14)", "surface": "oklch(0.18 0.005 253)", "surface-raised": "oklch(0.213 0.007 253)", "glass": "oklch(0.18 0.005 253 / 0.72)", "font-button": "var(--font-mono)", "weight-button": "500", "tracking-button": "0.16em", "case-button": "uppercase", "text-button-sm": "0.6875rem", "text-button": "0.75rem", "text-button-lg": "0.75rem", "font-badge": "var(--font-mono)", "weight-badge": "400", "tracking-badge": "0.12em", "case-badge": "uppercase", "text-badge": "0.6875rem", "vinyl": "oklch(0.963 0.003 68)", "sheet": "oklch(0.18 0.005 253)", "sheet-line": "oklch(0.277 0.008 253)", "cut": "oklch(0.337 0.01 253)" }, "dark": { "background": "oklch(0.144 0.003 264)", "foreground": "oklch(0.963 0.003 68)", "card": "oklch(0.18 0.005 253)", "card-foreground": "oklch(0.963 0.003 68)", "popover": "oklch(0.213 0.007 253)", "popover-foreground": "oklch(0.963 0.003 68)", "primary": "oklch(0.897 0.25 135)", "primary-foreground": "oklch(0.144 0.003 264)", "secondary": "oklch(0.235 0.008 253)", "secondary-foreground": "oklch(0.863 0.008 68)", "muted": "oklch(0.224 0.007 253)", "muted-foreground": "oklch(0.703 0.011 68)", "accent": "oklch(0.897 0.25 135)", "accent-foreground": "oklch(0.144 0.003 264)", "destructive": "oklch(0.641 0.218 29)", "destructive-foreground": "oklch(0.963 0.003 68)", "border": "oklch(0.277 0.008 253)", "input": "oklch(0.277 0.008 253)", "ring": "oklch(0.897 0.25 135)", "chart-1": "oklch(0.897 0.25 135)", "chart-2": "oklch(0.75 0.14 220)", "chart-3": "oklch(0.703 0.011 68)", "chart-4": "oklch(0.641 0.218 29)", "chart-5": "oklch(0.863 0.008 68)", "glow": "0 0 28px oklch(0.897 0.25 135 / 0.16)", "glow-primary": "0 0 0 1px oklch(0.897 0.25 135 / 0.5), 0 12px 40px -12px oklch(0.897 0.25 135 / 0.6)", "sheen": "oklch(0.897 0.25 135 / 0.14)", "surface": "oklch(0.18 0.005 253)", "surface-raised": "oklch(0.213 0.007 253)", "glass": "oklch(0.18 0.005 253 / 0.72)", "sheet": "oklch(0.18 0.005 253)", "sheet-line": "oklch(0.277 0.008 253)", "cut": "oklch(0.337 0.01 253)" } } }, { "name": "use-holo-pointer", "type": "registry:hook", "title": "useHoloPointer", "description": "Writes pointer position into CSS variables (--fx, --fy, --rx, --ry, --mx, --my) inside a single animation frame, so foil, tilt and magnetism run in CSS with no React re-renders. Honors prefers-reduced-motion.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/hooks/use-holo-pointer.ts", "type": "registry:hook" } ] }, { "name": "quack-button", "type": "registry:ui", "title": "Quack Button", "description": "Button with a full motion cycle: an idle animation while it waits, magnetic pull toward the pointer, a squash on press, and animated loading, success and error states. Sizes run from xs and icon-xs — the 24–28px instrument scale a control rail is built from, radius and icon size included — up to lg. The loading mark is swappable, so a theme with no mascot can drop in a plain spinner.", "dependencies": [ "class-variance-authority", "@radix-ui/react-slot", "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/duck-spinner", "@duck/use-holo-pointer" ], "files": [ { "path": "registry/duck/ui/quack-button.tsx", "type": "registry:ui" } ] }, { "name": "holo-button", "type": "registry:ui", "title": "Holo Button", "description": "The signature CTA: an animated iridescent border on a plain button. Variants: holo, primary, outline, ghost. One holo element per viewport.", "dependencies": [ "class-variance-authority", "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/holo-button.tsx", "type": "registry:ui" } ] }, { "name": "copy-button", "type": "registry:ui", "title": "Copy Button", "description": "Copies a string to the clipboard and confirms it with a check that pops in and settles back after two seconds. A refused write — plain HTTP, an embedded browser, a denied prompt — announces errorLabel and reports through onError, so the button never just appears to do nothing.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/copy-button.tsx", "type": "registry:ui" } ] }, { "name": "hud-chip", "type": "registry:ui", "title": "Hud Chip", "description": "The interactive HUD label: nav items, row actions, a zoom cluster, retry, esc. Mono, uppercase and tracked like HudLabel, because a chip and a label in the same row must not drift apart, and a real button underneath. Variants outline, ghost, primary; sizes xs, sm and default; active paints the current read and leaves the ARIA to the call site; asChild for a Link; frame={false} drops the die-cut edge.", "dependencies": [ "class-variance-authority", "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme", "@duck/hud-label" ], "files": [ { "path": "registry/duck/ui/hud-chip.tsx", "type": "registry:ui" } ] }, { "name": "sticker-card", "type": "registry:ui", "title": "Sticker Card", "description": "The die-cut sticker: thick border, generous radius, soft glow. Optional iridescent ring, pointer tilt, corner ticks, a translucent surface, a corner that peels off the backing on hover, asChild for a whole-card link, and frame={false} for a card nested inside a surface that is already the frame.", "dependencies": [ "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme", "@duck/use-holo-pointer" ], "files": [ { "path": "registry/duck/ui/sticker-card.tsx", "type": "registry:ui" } ] }, { "name": "sticker-media-card", "type": "registry:ui", "title": "Sticker Media Card", "description": "An artwork-first tile: the poster fills the die-cut frame edge to edge, the caption sits outside it, and the whole thing is one focusable link. Optional centred overlay and a thin progress bar along the bottom of the art.", "dependencies": [ "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme", "@duck/sticker-progress" ], "files": [ { "path": "registry/duck/ui/sticker-media-card.tsx", "type": "registry:ui" } ] }, { "name": "sticker-carousel", "type": "registry:ui", "title": "Sticker Carousel", "description": "A strip of stickers peeled off the roll sideways: a native scroll-snap track with arrows that grey out at each end, edge fades on whichever side is hiding content, keyboard paging, and no controls at all when the strip already fits.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/sticker-carousel.tsx", "type": "registry:ui" } ] }, { "name": "code-window", "type": "registry:ui", "title": "Code Window", "description": "Code block in a window frame with a filename, an optional copy control and optional line numbers. Takes plain text or pre-highlighted markup.", "registryDependencies": [ "@duck/theme", "@duck/copy-button" ], "files": [ { "path": "registry/duck/ui/code-window.tsx", "type": "registry:ui" } ] }, { "name": "code-snippet", "type": "registry:ui", "title": "Code Snippet", "description": "Self-highlighting code block with six color schemes, line numbers, line highlighting, collapsing, wrapping and PNG export. The highlighter is local, so there is no async theme load and no flash of unstyled code. A refused clipboard write tells the reader to select the code instead, and reports through onCopyError.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/copy-button" ], "files": [ { "path": "registry/duck/ui/code-snippet.tsx", "type": "registry:ui" }, { "path": "registry/duck/ui/code-highlight.ts", "type": "registry:ui" } ] }, { "name": "terminal", "type": "registry:ui", "title": "Terminal", "description": "A command line that types itself when it scrolls into view. Prints the full transcript at once under prefers-reduced-motion.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/terminal.tsx", "type": "registry:ui" } ] }, { "name": "sticker-sheet", "type": "registry:ui", "title": "Sticker Sheet", "description": "Backing paper with kiss-cut lines. Lays out a set of components the way a vinyl sheet lays out stickers.", "registryDependencies": [ "@duck/theme", "@duck/hud-label" ], "files": [ { "path": "registry/duck/ui/sticker-sheet.tsx", "type": "registry:ui" } ] }, { "name": "sticker-dialog", "type": "registry:ui", "title": "Sticker Dialog", "description": "Modal dialog on Radix Dialog, so the focus trap, scroll lock, escape handling and aria wiring are real. duck supplies the frosted scrim, the die-cut edge and the rise into place. Width presets sm to lg, plus a full-bleed full that drops the centring translate and fades instead of rising.", "dependencies": [ "@radix-ui/react-dialog", "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/sticker-dialog.tsx", "type": "registry:ui" } ] }, { "name": "sticker-drawer", "type": "registry:ui", "title": "Sticker Drawer", "description": "Edge-anchored panel on the same Radix Dialog base as StickerDialog, so the focus trap, scroll lock, escape handling and aria wiring are real. Anchors to any of the four sides, slides from the edge it is anchored to, and carries the die-cut edge on the side facing the content only. Sizes sm to lg plus a full-bleed full.", "dependencies": [ "class-variance-authority", "@radix-ui/react-dialog", "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "css": { "@layer utilities": { ".holo-edge": { "border-color": "transparent", "background": "linear-gradient(var(--card), var(--card)) padding-box, var(--holo) border-box" } }, "@keyframes duck-drawer-in": { "from": { "opacity": "0", "transform": "translate(var(--drawer-x, 0), var(--drawer-y, 0))" }, "to": { "opacity": "1", "transform": "translate(0, 0)" } }, "@keyframes duck-drawer-out": { "from": { "opacity": "1", "transform": "translate(0, 0)" }, "to": { "opacity": "0", "transform": "translate(var(--drawer-x, 0), var(--drawer-y, 0))" } } }, "files": [ { "path": "registry/duck/ui/sticker-drawer.tsx", "type": "registry:ui" } ] }, { "name": "sticker-popover", "type": "registry:ui", "title": "Sticker Popover", "description": "Panel anchored to its trigger, on Radix Popover, so focus return, escape, outside click and the aria wiring are real. duck supplies the 3px die-cut edge, the glow and the arrival, and exports STICKER_SURFACE — the class recipe that makes a stock shadcn menu or select match the same edge.", "dependencies": [ "@radix-ui/react-popover" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/sticker-popover.tsx", "type": "registry:ui" } ] }, { "name": "video-card", "type": "registry:ui", "title": "Video Card", "description": "A YouTube card that stays cheap until it is wanted. The player iframe only mounts on click, or pass href and the card navigates out instead of embedding.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/video-card.tsx", "type": "registry:ui" } ] }, { "name": "holo-avatar", "type": "registry:ui", "title": "Holo Avatar", "description": "A die-cut avatar sticker whose foil ring picks up the pointer. Includes a group that fans out on hover.", "dependencies": [ "class-variance-authority" ], "registryDependencies": [ "@duck/theme", "@duck/use-holo-pointer" ], "files": [ { "path": "registry/duck/ui/holo-avatar.tsx", "type": "registry:ui" } ] }, { "name": "holo-badge", "type": "registry:ui", "title": "Holo Badge", "description": "Pill for status, counts and short labels. Variants: holo, primary, outline, muted, success, danger. Shapes: pill for a status, tag for taxonomy that should follow the radius scale, block for a rail-wide status strip. asChild when the badge is semantically a heading, a dd or a link.", "dependencies": [ "class-variance-authority", "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/holo-badge.tsx", "type": "registry:ui" } ] }, { "name": "hud-label", "type": "registry:ui", "title": "Hud Label", "description": "The instrument-panel label: tiny, mono, uppercase, tracked wide enough to read as machine output rather than prose. Tones muted, foreground, primary and accent; an optional status square that takes the label's own colour; asChild for the section heading that is semantically an h3. Ships a matching .hud utility for the many places the label is an attribute of an existing element rather than an element of its own.", "dependencies": [ "class-variance-authority", "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme" ], "css": { "@layer utilities": { ".hud": { "font-family": "var(--font-mono)", "font-size": "0.6875rem", "font-weight": "400", "letter-spacing": "0.3em", "line-height": "1", "text-transform": "uppercase" }, ":where(.hud)": { "color": "var(--muted-foreground)" }, ".hud-sm": { "font-size": "0.625rem" }, ".hud-tight": { "letter-spacing": "0.18em" } } }, "files": [ { "path": "registry/duck/ui/hud-label.tsx", "type": "registry:ui" } ] }, { "name": "hud-code", "type": "registry:ui", "title": "Hud Code", "description": "The inline citation chip: a monospace token inside a sentence, tinted with --primary so it reads as verifiable data rather than as a snippet of source. Renders a code element by default; interactive makes it a button and asChild takes a link, for the click-a-citation case. Sized and spaced so it cannot change the leading of the paragraph it sits in.", "dependencies": [ "@radix-ui/react-slot" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/hud-code.tsx", "type": "registry:ui" } ] }, { "name": "announcement", "type": "registry:ui", "title": "Announcement", "description": "Pill banner for one piece of news, with a light that sweeps across it on a slow loop.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/announcement.tsx", "type": "registry:ui" } ] }, { "name": "duck-spinner", "type": "registry:ui", "title": "Duck Spinner", "description": "The duck/ui logo paddling on water, with the wake as expanding rings. Ships the mark into your public folder so the loading path is same-origin. Pass src to spin any image URL instead. Also exports DuckGlyph, the mark used by loading states.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/duck-spinner.tsx", "type": "registry:ui" }, { "path": "public/duck.svg", "target": "~/public/duck.svg", "type": "registry:file" } ] }, { "name": "holo-separator", "type": "registry:ui", "title": "Holo Separator", "description": "A hairline that fades in from the edges. With a label it becomes a section break.", "registryDependencies": [ "@duck/theme", "@duck/hud-label" ], "files": [ { "path": "registry/duck/ui/holo-separator.tsx", "type": "registry:ui" } ] }, { "name": "glow-input", "type": "registry:ui", "title": "Glow Input", "description": "Input, textarea and two field wrappers that wire up label, helper text, errors, aria-invalid and aria-describedby. GlowField wraps one control; GlowFieldset emits a real fieldset and legend for groups. frame={false} drops the sticker edge and the focus glow for a field inside a surface that is already the frame. Exports its own class strings, so a second control that has to read as the same field imports the recipe rather than copying it.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/glow-input.tsx", "type": "registry:ui" } ] }, { "name": "glow-search", "type": "registry:ui", "title": "Glow Search", "description": "A search field rather than a text field: leading icon, a clear button that appears with the first character and returns focus to the input, an optional ⌘K keycap hint, and a debounced onSearch. Typing debounces; Enter, Escape and clear flush. The frame is on the wrapper and the field inside it is frame={false}, so one edge holds the lot. The hint is a hint — the shortcut belongs to DuckCommand.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/glow-input", "@duck/sticker-kbd" ], "files": [ { "path": "registry/duck/ui/glow-search.tsx", "type": "registry:ui" } ] }, { "name": "glow-select", "type": "registry:ui", "title": "Glow Select", "description": "The select the field family was missing. The trigger wears GlowInput's own class strings and the menu wears StickerPopover's own surface — both imported, so neither can drift from the component it came from. Radix Select underneath for typeahead, roving highlight, collision-aware positioning and a hidden native select that still submits. Sizes sm and default, frame={false} for a select that is a row action.", "dependencies": [ "@radix-ui/react-select", "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/glow-input", "@duck/sticker-popover", "@duck/hud-label" ], "files": [ { "path": "registry/duck/ui/glow-select.tsx", "type": "registry:ui" } ] }, { "name": "glow-color", "type": "registry:ui", "title": "Glow Color", "description": "The colour field a design tool has more of than any other control. The four rules that make input[type=color] stop looking like an OS widget live here, so the swatch is a die-cut object with the lime focus glow, and the Chromium eyedropper appears beside it wherever the browser has one. Hex in, hex out, shorthand normalised.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/glow-input" ], "files": [ { "path": "registry/duck/ui/glow-color.tsx", "type": "registry:ui" } ] }, { "name": "duck-tabs", "type": "registry:ui", "title": "Duck Tabs", "description": "Tabs with an indicator that slides between triggers. Arrow keys move, Home and End jump to the ends. orientation=vertical is a real axis change — the list measures the vertical axis, Up and Down move, and the indicator becomes a bar down the left edge — so a settings dialog can keep its section rail.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/duck-tabs.tsx", "type": "registry:ui" } ] }, { "name": "theme-switcher", "type": "registry:ui", "title": "Theme Switcher", "description": "Three-way segmented control for light, dark and system, with a lime pill that slides to the active option. Needs a next-themes provider.", "dependencies": [ "next-themes", "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/theme-switcher.tsx", "type": "registry:ui" } ] }, { "name": "quack-toast", "type": "registry:ui", "title": "Quack Toast", "description": "Toast provider and hook. Messages slide in from the corner, never steal focus, and the queue is capped so a burst of events cannot bury the page.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/duck-spinner" ], "files": [ { "path": "registry/duck/ui/quack-toast.tsx", "type": "registry:ui" } ] }, { "name": "duck-switch", "type": "registry:ui", "title": "Duck Switch", "description": "Toggle built on a real checkbox, so it submits with the form. Off it rests on the bank, on the track floods lime and a ripple spreads from the entry point. Sizes: sm, default.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/duck-switch.tsx", "type": "registry:ui" } ] }, { "name": "sticker-checkbox", "type": "registry:ui", "title": "Sticker Checkbox", "description": "A kiss-cut square you apply rather than fill in: the tick lands with a squash and the box takes its vinyl edge in the same beat. Real checkbox underneath, with indeterminate support.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/sticker-checkbox.tsx", "type": "registry:ui" } ] }, { "name": "sticker-radio-group", "type": "registry:ui", "title": "Sticker Radio Group", "description": "A strip of kiss-cut cells where only the chosen sticker has been peeled off the backing. Native radios, so arrow keys and roving tab order come from the browser.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/sticker-radio-group.tsx", "type": "registry:ui" } ] }, { "name": "sticker-toggle-group", "type": "registry:ui", "title": "Sticker Toggle Group", "description": "A segmented control cut from one strip of vinyl: the sticker border belongs to the whole set and only the chosen panel takes the lime fill. Roving tabindex and arrow keys, radio semantics for single select and aria-pressed for multiple.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/sticker-toggle-group.tsx", "type": "registry:ui" } ] }, { "name": "duck-slider", "type": "registry:ui", "title": "Duck Slider", "description": "The waterline: the filled track is water, the thumb floats on it, and letting go leaves a wake. A real input type=range, so arrows, PageUp/PageDown, Home/End, touch and RTL are the browser's job. curve=log owns the geometric mapping a font-size or zoom slider needs, and keeps aria-valuetext honest; label, valuePosition=row and action put the readout and a reset on the label row instead of above the track. Single value.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/duck-slider.tsx", "type": "registry:ui" } ] }, { "name": "duck-media-slider", "type": "registry:ui", "title": "Duck Media Slider", "description": "The waterline along the edge of a film: a dimmer second fill for what has loaded, a scrub readout that runs ahead of the thumb, and a dense 4px variant that only grows a duck when you reach for it. Still a real input type=range, and it holds the value while you drag so the element's own timeupdate cannot fight the thumb.", "registryDependencies": [ "@duck/theme" ], "files": [ { "path": "registry/duck/ui/duck-media-slider.tsx", "type": "registry:ui" } ] }, { "name": "duck-volume", "type": "registry:ui", "title": "Duck Volume", "description": "The tap: a mute toggle with the water hidden behind it, opening sideways on hover or focus. Keeps video.muted and video.volume in sync, treats volume 0 as silence, and hands back the last audible level when you unmute. Built on DuckMediaSlider dense.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/duck-media-slider", "@duck/quack-button" ], "files": [ { "path": "registry/duck/ui/duck-volume.tsx", "type": "registry:ui" } ] }, { "name": "duck-audio-player", "type": "registry:ui", "title": "Duck Audio Player", "description": "The themed replacement for a native