{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reveal-button",
  "type": "registry:ui",
  "title": "RevealButton",
  "description": "Motion-powered icon-first button that reveals its label on hover and focus.",
  "dependencies": [
    "motion@^12.43.0"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/button.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/reveal-button/reveal-button.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/reveal-button/reveal-button.tsx",
      "content": "import {\n  cloneElement,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type ButtonHTMLAttributes,\n  type FocusEventHandler,\n  type MouseEventHandler,\n  type PointerEventHandler,\n  type ReactNode,\n} from \"react\";\nimport {\n  motion as motionElement,\n  useReducedMotion,\n  type HTMLMotionProps,\n  type TargetAndTransition,\n  type Transition,\n} from \"motion/react\";\nimport { Pressable } from \"react-aria-components\";\nimport type { ButtonVariant } from \"../button\";\nimport { cn } from \"../../utils/cn\";\nimport { useHydrated } from \"../../utils/use-hydrated\";\n\nexport type RevealButtonVariant = Exclude<ButtonVariant, \"link\">;\nexport type RevealButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type RevealButtonMotion = \"none\" | \"subtle\" | \"standard\";\nexport type RevealButtonLabelVisibility = \"hover\" | \"always\";\n\nexport interface RevealButtonProps extends Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  \"aria-label\" | \"aria-labelledby\" | \"children\"\n> {\n  icon: ReactNode;\n  label: string;\n  labelVisibility?: RevealButtonLabelVisibility;\n  loading?: boolean;\n  motion?: RevealButtonMotion;\n  size?: RevealButtonSize;\n  variant?: RevealButtonVariant;\n}\n\nconst revealButtonBaseClasses =\n  \"relative inline-flex shrink-0 transform-gpu items-center justify-start overflow-hidden whitespace-nowrap rounded-md border border-transparent font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[loading=true]:cursor-wait data-[loading=true]:opacity-80 data-[reduced-motion=true]:will-change-auto\";\n\nconst revealButtonVariantClasses: Record<RevealButtonVariant, string> = {\n  solid:\n    \"bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80\",\n  soft: \"bg-primary/10 text-primary hover:bg-primary/15 active:bg-primary/20\",\n  outline:\n    \"border-border bg-background text-foreground hover:bg-muted active:bg-muted/80\",\n  ghost: \"bg-transparent text-foreground hover:bg-muted active:bg-muted/80\",\n  destructive:\n    \"bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80\",\n};\n\nconst revealButtonSizeClasses: Record<RevealButtonSize, string> = {\n  xs: \"h-7 min-w-7 text-xs\",\n  sm: \"h-8 min-w-8 text-sm\",\n  md: \"h-density-control min-w-density-control text-sm\",\n  lg: \"h-11 min-w-11 text-base\",\n  xl: \"h-12 min-w-12 text-base\",\n};\n\nconst revealButtonIconBoxClasses: Record<RevealButtonSize, string> = {\n  xs: \"size-7\",\n  sm: \"size-8\",\n  md: \"size-density-control\",\n  lg: \"size-11\",\n  xl: \"size-12\",\n};\n\nconst revealButtonIconSizeClasses: Record<RevealButtonSize, string> = {\n  xs: \"size-3.5 [&>svg]:size-3.5\",\n  sm: \"size-4 [&>svg]:size-4\",\n  md: \"size-4 [&>svg]:size-4\",\n  lg: \"size-5 [&>svg]:size-5\",\n  xl: \"size-5 [&>svg]:size-5\",\n};\n\nconst revealButtonIconRevealShift: Record<RevealButtonSize, number> = {\n  xs: 3,\n  sm: 4,\n  md: 5,\n  lg: 6,\n  xl: 6,\n};\n\nconst revealButtonLabelPaddingClasses: Record<RevealButtonSize, string> = {\n  xs: \"ps-[var(--dt-space-1)] pe-[var(--dt-space-2)]\",\n  sm: \"ps-[var(--dt-space-1)] pe-[var(--dt-space-3)]\",\n  md: \"ps-[var(--dt-space-2)] pe-[var(--dt-space-4)]\",\n  lg: \"ps-[var(--dt-space-2)] pe-[var(--dt-space-5)]\",\n  xl: \"ps-[var(--dt-space-2)] pe-[var(--dt-space-5)]\",\n};\n\nconst revealButtonIconClasses =\n  \"pointer-events-none inline-flex shrink-0 items-center justify-center\";\n\nconst revealButtonIconMotionClasses = \"inline-flex items-center justify-center\";\n\nconst revealButtonSpinnerClasses =\n  \"pointer-events-none shrink-0 rounded-full border-2 border-current border-r-transparent animate-spin motion-reduce:animate-none\";\n\nconst revealButtonLabelClasses =\n  \"pointer-events-none min-w-0 shrink-0 overflow-hidden\";\n\nconst revealButtonLabelTextClasses = \"inline-block whitespace-nowrap\";\n\nconst revealButtonLabelMeasureClasses =\n  \"pointer-events-none invisible absolute inset-y-0 start-0 inline-flex w-max items-center whitespace-nowrap\";\n\nconst revealButtonTransitions: Record<RevealButtonMotion, Transition> = {\n  none: { duration: 0 },\n  subtle: { type: \"spring\", stiffness: 440, damping: 36, mass: 0.8 },\n  standard: { type: \"spring\", stiffness: 520, damping: 34, mass: 0.75 },\n};\n\nconst revealButtonLabelTransitions: Record<RevealButtonMotion, Transition> = {\n  none: { duration: 0 },\n  subtle: { type: \"spring\", stiffness: 360, damping: 34, mass: 0.9 },\n  standard: { type: \"spring\", stiffness: 420, damping: 32, mass: 0.85 },\n};\n\nconst revealButtonEstimatedFontSize: Record<RevealButtonSize, number> = {\n  xs: 12,\n  sm: 14,\n  md: 14,\n  lg: 16,\n  xl: 16,\n};\n\nconst revealButtonEstimatedLabelPadding: Record<RevealButtonSize, number> = {\n  xs: 12,\n  sm: 16,\n  md: 24,\n  lg: 28,\n  xl: 28,\n};\n\nexport function revealButtonClassNames({\n  variant = \"ghost\",\n  size = \"md\",\n  className,\n}: Pick<RevealButtonProps, \"variant\" | \"size\" | \"className\"> = {}) {\n  return cn(\n    revealButtonBaseClasses,\n    revealButtonSizeClasses[size],\n    revealButtonVariantClasses[variant],\n    className,\n  );\n}\n\nfunction estimateLabelInlineSize(label: string, size: RevealButtonSize) {\n  return Math.ceil(\n    Math.max(label.length, 1) * revealButtonEstimatedFontSize[size] * 0.56 +\n      revealButtonEstimatedLabelPadding[size],\n  );\n}\n\nfunction measureInlineSize(node: HTMLElement) {\n  return Math.ceil(node.scrollWidth || node.getBoundingClientRect().width);\n}\n\nfunction useMeasuredInlineSize(label: string) {\n  const ref = useRef<HTMLSpanElement | null>(null);\n  const [inlineSize, setInlineSize] = useState(0);\n\n  useEffect(() => {\n    const node = ref.current;\n\n    if (!node) {\n      return undefined;\n    }\n\n    const measure = () => {\n      setInlineSize(measureInlineSize(node));\n    };\n\n    measure();\n\n    if (typeof ResizeObserver === \"undefined\") {\n      return undefined;\n    }\n\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [label]);\n\n  return [ref, inlineSize] as const;\n}\n\nfunction getPressMotionTarget({\n  disabled,\n  reducedMotion,\n}: {\n  disabled: boolean;\n  reducedMotion: boolean;\n}): TargetAndTransition | undefined {\n  if (disabled || reducedMotion) {\n    return undefined;\n  }\n\n  return { scale: 0.96 };\n}\n\nfunction renderSizedIcon(icon: ReactNode) {\n  if (!isValidElement<{ className?: string }>(icon)) {\n    return icon;\n  }\n\n  return cloneElement(icon, {\n    className: cn(icon.props.className, \"size-full shrink-0\"),\n  });\n}\n\nfunction canRevealForHover() {\n  if (\n    typeof window === \"undefined\" ||\n    typeof window.matchMedia !== \"function\"\n  ) {\n    return true;\n  }\n\n  return window.matchMedia(\"(hover: hover)\").matches;\n}\n\nexport const RevealButton = forwardRef<HTMLButtonElement, RevealButtonProps>(\n  (\n    {\n      \"aria-busy\": ariaBusy,\n      \"aria-disabled\": ariaDisabled,\n      className,\n      disabled = false,\n      icon,\n      label,\n      labelVisibility = \"hover\",\n      loading = false,\n      motion = \"standard\",\n      onBlur,\n      onClick,\n      onFocus,\n      onMouseEnter,\n      onMouseLeave,\n      onPointerEnter,\n      onPointerLeave,\n      size = \"md\",\n      type = \"button\",\n      variant = \"ghost\",\n      ...props\n    },\n    ref,\n  ) => {\n    const buttonRef = useRef<HTMLButtonElement | null>(null);\n    const prefersReducedMotion = useReducedMotion();\n    const hasHydrated = useHydrated();\n    const reducedMotion =\n      motion === \"none\" || (hasHydrated && prefersReducedMotion === true);\n    const isAriaDisabled = ariaDisabled === true || ariaDisabled === \"true\";\n    const isActivationDisabled = disabled || loading || isAriaDisabled;\n    const canReveal = !disabled;\n    const [hovered, setHovered] = useState(false);\n    const [focused, setFocused] = useState(false);\n    const [isRtl, setIsRtl] = useState(false);\n    const [labelRef, measuredLabelInlineSize] = useMeasuredInlineSize(label);\n    const revealed = labelVisibility === \"always\" || hovered || focused;\n    const revealState = revealed ? \"revealed\" : \"collapsed\";\n    const transition = reducedMotion\n      ? revealButtonTransitions.none\n      : revealButtonTransitions[motion];\n    const labelTransition = reducedMotion\n      ? revealButtonLabelTransitions.none\n      : revealButtonLabelTransitions[motion];\n    const resolvedLabelInlineSize =\n      measuredLabelInlineSize > 0\n        ? measuredLabelInlineSize\n        : estimateLabelInlineSize(label, size);\n    const labelWidth = resolvedLabelInlineSize;\n\n    const setButtonRef = useCallback(\n      (node: HTMLButtonElement | null) => {\n        buttonRef.current = node;\n\n        if (typeof ref === \"function\") {\n          ref(node);\n        } else if (ref) {\n          ref.current = node;\n        }\n      },\n      [ref],\n    );\n\n    useEffect(() => {\n      const node = buttonRef.current;\n\n      if (!node) {\n        return undefined;\n      }\n\n      const syncCurrentInteractionState = () => {\n        setFocused(node === document.activeElement);\n        setHovered(canReveal && canRevealForHover() && node.matches(\":hover\"));\n        setIsRtl(getComputedStyle(node).direction === \"rtl\");\n      };\n\n      syncCurrentInteractionState();\n\n      const frame =\n        typeof window.requestAnimationFrame === \"function\"\n          ? window.requestAnimationFrame(syncCurrentInteractionState)\n          : undefined;\n\n      const handleNativeFocus = () => {\n        setFocused(true);\n      };\n\n      const handleNativeBlur = () => {\n        setFocused(false);\n      };\n\n      const handleNativePointerEnter = (event: PointerEvent) => {\n        if (canReveal && event.pointerType !== \"touch\" && canRevealForHover()) {\n          setHovered(true);\n        }\n      };\n\n      const handleNativePointerLeave = () => {\n        setHovered(false);\n      };\n\n      node.addEventListener(\"focus\", handleNativeFocus);\n      node.addEventListener(\"blur\", handleNativeBlur);\n      node.addEventListener(\"pointerenter\", handleNativePointerEnter);\n      node.addEventListener(\"pointerleave\", handleNativePointerLeave);\n\n      return () => {\n        if (frame !== undefined) {\n          window.cancelAnimationFrame(frame);\n        }\n\n        node.removeEventListener(\"focus\", handleNativeFocus);\n        node.removeEventListener(\"blur\", handleNativeBlur);\n        node.removeEventListener(\"pointerenter\", handleNativePointerEnter);\n        node.removeEventListener(\"pointerleave\", handleNativePointerLeave);\n      };\n    }, [canReveal]);\n\n    if (disabled && (hovered || focused)) {\n      setHovered(false);\n      setFocused(false);\n    }\n\n    const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => {\n      if (isActivationDisabled) {\n        event.preventDefault();\n        return;\n      }\n\n      onClick?.(event);\n    };\n\n    const handleFocus: FocusEventHandler<HTMLButtonElement> = (event) => {\n      setFocused(true);\n      onFocus?.(event);\n    };\n\n    const handleBlur: FocusEventHandler<HTMLButtonElement> = (event) => {\n      setFocused(false);\n      onBlur?.(event);\n    };\n\n    const handlePointerEnter: PointerEventHandler<HTMLButtonElement> = (\n      event,\n    ) => {\n      if (canReveal && event.pointerType !== \"touch\" && canRevealForHover()) {\n        setHovered(true);\n      }\n\n      onPointerEnter?.(event);\n    };\n\n    const handlePointerLeave: PointerEventHandler<HTMLButtonElement> = (\n      event,\n    ) => {\n      setHovered(false);\n      onPointerLeave?.(event);\n    };\n\n    const handleMouseEnter: MouseEventHandler<HTMLButtonElement> = (event) => {\n      if (canReveal && canRevealForHover()) {\n        setHovered(true);\n      }\n\n      onMouseEnter?.(event);\n    };\n\n    const handleMouseLeave: MouseEventHandler<HTMLButtonElement> = (event) => {\n      setHovered(false);\n      onMouseLeave?.(event);\n    };\n    const iconRevealShift = reducedMotion\n      ? 0\n      : revealButtonIconRevealShift[size] * (isRtl ? -1 : 1);\n    const iconMotionVariants = {\n      collapsed: { x: 0 },\n      revealed: { x: iconRevealShift },\n    };\n\n    return (\n      <Pressable isDisabled={isActivationDisabled}>\n        <motionElement.button\n          {...(props as HTMLMotionProps<\"button\">)}\n          ref={setButtonRef}\n          type={type}\n          disabled={disabled}\n          aria-label={label}\n          aria-busy={loading ? true : ariaBusy}\n          aria-disabled={loading ? true : ariaDisabled}\n          data-slot=\"reveal-button\"\n          data-variant={variant}\n          data-size={size}\n          data-motion={motion}\n          data-label-visibility={labelVisibility}\n          data-state={revealState}\n          data-disabled={disabled ? \"true\" : undefined}\n          data-loading={loading ? \"true\" : undefined}\n          data-reduced-motion={reducedMotion ? \"true\" : undefined}\n          className={revealButtonClassNames({ variant, size, className })}\n          variants={{ collapsed: {}, revealed: {} }}\n          initial={false}\n          animate={revealState}\n          whileHover={canReveal ? \"revealed\" : undefined}\n          whileFocus={canReveal ? \"revealed\" : undefined}\n          whileTap={getPressMotionTarget({\n            disabled: isActivationDisabled,\n            reducedMotion,\n          })}\n          transition={transition}\n          onBlur={handleBlur}\n          onClick={handleClick}\n          onFocus={handleFocus}\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={handleMouseLeave}\n          onPointerEnter={handlePointerEnter}\n          onPointerLeave={handlePointerLeave}\n        >\n          {loading ? (\n            <span\n              aria-hidden=\"true\"\n              data-slot=\"reveal-button-spinner\"\n              className={cn(\n                revealButtonIconClasses,\n                revealButtonIconBoxClasses[size],\n              )}\n            >\n              <motionElement.span\n                data-slot=\"reveal-button-spinner-motion\"\n                className={cn(\n                  revealButtonIconMotionClasses,\n                  revealButtonIconSizeClasses[size],\n                )}\n                variants={iconMotionVariants}\n                transition={transition}\n              >\n                <span\n                  data-slot=\"reveal-button-spinner-glyph\"\n                  className={cn(revealButtonSpinnerClasses, \"size-full\")}\n                />\n              </motionElement.span>\n            </span>\n          ) : (\n            <span\n              aria-hidden=\"true\"\n              data-slot=\"reveal-button-icon\"\n              className={cn(\n                revealButtonIconClasses,\n                revealButtonIconBoxClasses[size],\n              )}\n            >\n              <motionElement.span\n                data-slot=\"reveal-button-icon-motion\"\n                className={cn(\n                  revealButtonIconMotionClasses,\n                  revealButtonIconSizeClasses[size],\n                )}\n                variants={iconMotionVariants}\n                transition={transition}\n              >\n                {renderSizedIcon(icon)}\n              </motionElement.span>\n            </span>\n          )}\n          <motionElement.span\n            aria-hidden=\"true\"\n            data-slot=\"reveal-button-label\"\n            className={revealButtonLabelClasses}\n            variants={{\n              collapsed: {\n                width: labelVisibility === \"always\" ? labelWidth : 0,\n                opacity: labelVisibility === \"always\" ? 1 : 0,\n              },\n              revealed: {\n                width: labelWidth,\n                opacity: 1,\n              },\n            }}\n            transition={labelTransition}\n          >\n            <span\n              data-slot=\"reveal-button-label-text\"\n              className={cn(\n                revealButtonLabelTextClasses,\n                revealButtonLabelPaddingClasses[size],\n              )}\n            >\n              {label}\n            </span>\n          </motionElement.span>\n          <span\n            ref={labelRef}\n            aria-hidden=\"true\"\n            data-slot=\"reveal-button-label-measure\"\n            className={cn(\n              revealButtonLabelMeasureClasses,\n              revealButtonLabelPaddingClasses[size],\n            )}\n          >\n            {label}\n          </span>\n        </motionElement.button>\n      </Pressable>\n    );\n  },\n);\n\nRevealButton.displayName = \"RevealButton\";\n"
    },
    {
      "path": "packages/components/src/components/reveal-button/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/reveal-button/index.ts",
      "content": "export {\n  RevealButton,\n  revealButtonClassNames,\n  type RevealButtonLabelVisibility,\n  type RevealButtonMotion,\n  type RevealButtonProps,\n  type RevealButtonSize,\n  type RevealButtonVariant,\n} from \"./reveal-button\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
