{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"icon-button","type":"registry:ui","title":"Icon Button","description":"A square button that is only an icon – ghost, outlined or solid, two sizes, a pressed state for toggles and its label as the tooltip","dependencies":[],"files":[{"path":"registry/icon-button/icon-button.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { createElement, type ComponentPropsWithRef, type ReactNode } from \"react\";\n\n/*\n * Icon Button – a single self-contained file.\n * A square button that is only an icon: quiet ghost, a white button with a 1px edge, or a dark gradient. It can stay\n * pressed (a toggle), and its label is both the accessible name and the tooltip.\n * The styles are injected below, so no extra files are needed.\n */\n\n/** An icon from Hugeicons (e.g. `import { Search01Icon } from \"@hugeicons/core-free-icons\"`) – drawn inline */\nexport type HugeIcon = readonly (readonly [string, { readonly [key: string]: string | number }])[];\n\nexport type IconButtonProps = Omit<ComponentPropsWithRef<\"button\">, \"children\"> & {\n  /** The glyph: any element (an inline SVG) or a Hugeicons icon object */\n  icon: ReactNode | HugeIcon;\n  /** What the button does – read out by screen readers and shown as the tooltip */\n  label: string;\n  /** ghost: just the icon, a soft wash on hover · secondary: white with a 1px edge · primary: a dark gradient */\n  variant?: \"ghost\" | \"secondary\" | \"primary\";\n  /** sm: 32 × 32 · md: 40 × 40 – the same heights as the Button – the hit area grows on touch screens */\n  size?: \"sm\" | \"md\";\n  /** Makes it a toggle: true or false sets aria-pressed and keeps it looking pressed while true */\n  pressed?: boolean;\n};\n\nexport function IconButton({\n  icon,\n  label,\n  variant = \"ghost\",\n  size = \"md\",\n  pressed,\n  title,\n  className,\n  type = \"button\",\n  ...rest\n}: IconButtonProps) {\n  // Unknown values fall back to the defaults instead of an unstyled button\n  const tone = VARIANTS.includes(variant) ? variant : \"ghost\";\n  const scale = size === \"sm\" ? \"sm\" : \"md\";\n\n  return (\n    <>\n      {/* React 19 hoists this into <head> once, no matter how many buttons render */}\n      <style href=\"ui-icon-button\" precedence=\"default\">\n        {css}\n      </style>\n      <button\n        type={type}\n        className={[\"ui-icb\", className].filter(Boolean).join(\" \")}\n        data-variant={tone}\n        data-size={scale}\n        aria-label={label}\n        // The label doubles as the tooltip; pass title=\"\" to leave it out\n        title={title ?? label}\n        aria-pressed={pressed}\n        {...rest}\n      >\n        <span className=\"ui-icb__icon\" aria-hidden=\"true\">\n          <Glyph icon={icon} />\n        </span>\n      </button>\n    </>\n  );\n}\n\nconst VARIANTS = [\"ghost\", \"secondary\", \"primary\"];\n\nconst isHugeIcon = (icon: unknown): icon is HugeIcon =>\n  Array.isArray(icon) && icon.length > 0 && icon.every((part) => Array.isArray(part) && typeof part[0] === \"string\");\n\n/** Draws a Hugeicons icon object as an inline SVG at the button's stroke; anything else is shown as it is */\nfunction Glyph({ icon }: { icon: ReactNode | HugeIcon }) {\n  if (!isHugeIcon(icon)) return <>{icon}</>;\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" focusable=\"false\">\n      {icon.map(([tag, { key, ...attrs }], i) =>\n        createElement(tag, { ...attrs, key: String(key ?? i), ...(\"strokeWidth\" in attrs ? { strokeWidth: STROKE } : null) }),\n      )}\n    </svg>\n  );\n}\nconst STROKE = 1.75;\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors as variables – override them on .ui-icb (see Theming in the docs) */\n.ui-icb {\n  --ui-icb-icon: var(--aui-muted, #737373);\n  --ui-icb-ink: var(--aui-ink, #171717);\n  --ui-icb-hover: #404040;\n  --ui-icb-fill: #f0f0f0;\n  --ui-icb-pressed: #e8e8e8;\n  --ui-icb-surface: var(--aui-surface, #ffffff);\n  --ui-icb-surface-hover: #fafafa;\n  --ui-icb-surface-shadow: rgba(0, 0, 0, 0.06);\n  --ui-icb-line: var(--aui-border, #e5e5e5);\n  --ui-icb-on-ink: #fafafa;\n  --ui-icb-dark-top: #323137;\n  --ui-icb-dark-bottom: #201e25;\n  --ui-icb-dark-hover-top: #3c3b42;\n  --ui-icb-dark-hover-bottom: #29272f;\n  --ui-icb-edge-top: #4b4951;\n  --ui-icb-edge-mid: #31303a;\n  --ui-icb-edge-bottom: #18171b;\n  --ui-icb-ring: #0d0d0d;\n  --ui-icb-dark-shadow: rgba(0, 0, 0, 0.1);\n  --ui-icb-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-icb-ease: cubic-bezier(0.22, 1, 0.36, 1);\n\n  --size: 40px;\n  --icon: 18px;\n  --radius: var(--aui-radius, 12px);\n\n  position: relative;\n  display: inline-grid;\n  place-items: center;\n  flex: none;\n  box-sizing: border-box;\n  width: var(--size);\n  height: var(--size);\n  padding: 0;\n  margin: 0;\n  border: 1px solid transparent;\n  border-radius: var(--radius);\n  background: transparent;\n  color: var(--ui-icb-icon);\n  font: inherit;\n  cursor: pointer;\n  user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  transition:\n    background-color 160ms ease,\n    border-color 160ms ease,\n    color 160ms ease,\n    box-shadow 160ms ease,\n    transform 160ms var(--ui-icb-ease),\n    opacity 160ms ease;\n}\n.ui-icb[data-size=\"sm\"] {\n  --size: 32px;\n  --icon: 16px;\n  --radius: calc(var(--aui-radius, 12px) - 2px);\n}\n\n.ui-icb__icon {\n  display: grid;\n  place-items: center;\n  width: var(--icon);\n  height: var(--icon);\n  pointer-events: none;\n}\n.ui-icb__icon > svg,\n.ui-icb__icon > img {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n/* ghost: just the icon, the gray layer appears on hover */\n.ui-icb[data-variant=\"ghost\"]:hover:not(:disabled) {\n  background: var(--ui-icb-fill);\n  color: var(--ui-icb-hover);\n}\n.ui-icb[data-variant=\"ghost\"][aria-pressed=\"true\"] {\n  background: var(--ui-icb-pressed);\n  color: var(--ui-icb-ink);\n}\n\n/* secondary: white with the card outline */\n.ui-icb[data-variant=\"secondary\"] {\n  background: var(--ui-icb-surface);\n  border-color: var(--ui-icb-line);\n  box-shadow: 0 1px 2px var(--ui-icb-surface-shadow);\n}\n.ui-icb[data-variant=\"secondary\"]:hover:not(:disabled) {\n  background: var(--ui-icb-surface-hover);\n  color: var(--ui-icb-hover);\n}\n/* Pressed sinks into the gray layer, the shadow goes */\n.ui-icb[data-variant=\"secondary\"][aria-pressed=\"true\"] {\n  background: var(--ui-icb-fill);\n  box-shadow: none;\n  color: var(--ui-icb-ink);\n}\n\n/* primary: a dark gradient with a light edge running down the border and a 1px ring, like the Button */\n.ui-icb[data-variant=\"primary\"] {\n  background:\n    linear-gradient(var(--ui-icb-dark-top), var(--ui-icb-dark-bottom)) padding-box,\n    linear-gradient(var(--ui-icb-edge-top), var(--ui-icb-edge-mid), var(--ui-icb-edge-bottom)) border-box;\n  color: var(--ui-icb-on-ink);\n  box-shadow:\n    0 0 0 1px var(--ui-icb-ring),\n    0 2px 4px var(--ui-icb-dark-shadow);\n}\n.ui-icb[data-variant=\"primary\"]:hover:not(:disabled) {\n  background:\n    linear-gradient(var(--ui-icb-dark-hover-top), var(--ui-icb-dark-hover-bottom)) padding-box,\n    linear-gradient(var(--ui-icb-edge-top), var(--ui-icb-edge-mid), var(--ui-icb-edge-bottom)) border-box;\n}\n\n.ui-icb:active:not(:disabled) {\n  transform: scale(0.95);\n}\n.ui-icb:focus-visible {\n  outline: 2px solid var(--ui-icb-focus);\n  outline-offset: 2px;\n}\n.ui-icb:disabled {\n  opacity: 0.4;\n  cursor: default;\n}\n\n/* Touch screens: the small size gets an invisible ring around it that takes the tap (40 × 40) */\n@media (pointer: coarse) {\n  .ui-icb[data-size=\"sm\"]::after {\n    content: \"\";\n    position: absolute;\n    inset: -5px;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-icb {\n    transition-duration: 1ms;\n  }\n  .ui-icb:active:not(:disabled) {\n    transform: none;\n  }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-icb {\n  --ui-icb-icon: var(--aui-muted, #8f8f8f);\n  --ui-icb-ink: var(--aui-ink, #ededed);\n  --ui-icb-hover: #ededed;\n  --ui-icb-fill: rgba(255, 255, 255, 0.06);\n  --ui-icb-pressed: rgba(255, 255, 255, 0.1);\n  --ui-icb-surface: var(--aui-surface, #232326);\n  --ui-icb-surface-hover: #2a2a2d;\n  --ui-icb-surface-shadow: rgba(0, 0, 0, 0.4);\n  --ui-icb-line: var(--aui-border, rgba(255, 255, 255, 0.1));\n  /* primary inverts: a light gradient with dark glyph */\n  --ui-icb-on-ink: #121213;\n  --ui-icb-dark-top: #f4f4f5;\n  --ui-icb-dark-bottom: #e4e4e7;\n  --ui-icb-dark-hover-top: #ffffff;\n  --ui-icb-dark-hover-bottom: #ececef;\n  --ui-icb-edge-top: #ffffff;\n  --ui-icb-edge-mid: #ececef;\n  --ui-icb-edge-bottom: #d4d4d8;\n  --ui-icb-ring: rgba(0, 0, 0, 0.6);\n  --ui-icb-dark-shadow: rgba(0, 0, 0, 0.4);\n  --ui-icb-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n}\n`;\n"}]}