{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radial-button",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "radial-button.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst controlCornerClassName =\n  \"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst FILL_DURATION = 0.5;\nconst FILL_EASE = [0.16, 1, 0.3, 1] as const;\n\ntype ButtonHTMLAttributesForMotion = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  | \"onAnimationEnd\"\n  | \"onAnimationIteration\"\n  | \"onAnimationStart\"\n  | \"onDrag\"\n  | \"onDragEnd\"\n  | \"onDragEnter\"\n  | \"onDragExit\"\n  | \"onDragLeave\"\n  | \"onDragOver\"\n  | \"onDragStart\"\n  | \"onDrop\"\n>;\n\nfunction getCoverDiameter(width: number, height: number, x: number, y: number) {\n  return Math.ceil(\n    2 *\n      Math.max(\n        Math.hypot(x, y),\n        Math.hypot(width - x, y),\n        Math.hypot(x, height - y),\n        Math.hypot(width - x, height - y)\n      )\n  );\n}\n\nfunction assignRef<T>(ref: React.ForwardedRef<T>, value: T | null) {\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n\n  if (ref) {\n    ref.current = value;\n  }\n}\n\nfunction hasTextContent(node: React.ReactNode): boolean {\n  if (typeof node === \"string\" || typeof node === \"number\") {\n    return String(node).trim().length > 0;\n  }\n\n  if (Array.isArray(node)) {\n    return node.some(hasTextContent);\n  }\n\n  if (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n    return hasTextContent(node.props.children);\n  }\n\n  return false;\n}\n\ntype RadialButtonProps = ButtonHTMLAttributesForMotion & {\n  children?: React.ReactNode;\n  loading?: boolean;\n};\n\nconst RadialButton = React.forwardRef<HTMLButtonElement, RadialButtonProps>(\n  (\n    {\n      children,\n      className,\n      disabled = false,\n      loading = false,\n      type = \"button\",\n      onBlur,\n      onClick,\n      onFocus,\n      onKeyDown,\n      onKeyUp,\n      onPointerCancel,\n      onPointerDown,\n      onPointerEnter,\n      onPointerLeave,\n      onPointerUp,\n      ...props\n    },\n    ref\n  ) => {\n    const buttonRef = React.useRef<HTMLButtonElement>(null);\n    const isDisabled = Boolean(disabled || loading);\n    const [hovered, setHovered] = React.useState(false);\n    const [isPressed, setIsPressed] = React.useState(false);\n    const [origin, setOrigin] = React.useState({ x: 0, y: 0 });\n    const [coverSize, setCoverSize] = React.useState(0);\n\n    const ariaLabel = props[\"aria-label\"];\n    const ariaLabelledBy = props[\"aria-labelledby\"];\n\n    React.useEffect(() => {\n      if (process.env.NODE_ENV === \"production\") {\n        return;\n      }\n\n      if (\n        hasTextContent(children) ||\n        ariaLabel?.trim() ||\n        ariaLabelledBy?.trim()\n      ) {\n        return;\n      }\n\n      console.warn(\n        \"RadialButton: provide visible label text or aria-label / aria-labelledby so the control has an accessible name.\"\n      );\n    }, [ariaLabel, ariaLabelledBy, children]);\n\n    const updateOrigin = React.useCallback((x: number, y: number) => {\n      const node = buttonRef.current;\n      if (!node) return;\n\n      const rect = node.getBoundingClientRect();\n      setOrigin({ x, y });\n      setCoverSize(getCoverDiameter(rect.width, rect.height, x, y));\n    }, []);\n\n    const updateOriginFromPointer = React.useCallback(\n      (event: React.PointerEvent<HTMLButtonElement>) => {\n        const rect = event.currentTarget.getBoundingClientRect();\n        updateOrigin(event.clientX - rect.left, event.clientY - rect.top);\n      },\n      [updateOrigin]\n    );\n\n    const updateOriginFromCenter = React.useCallback(() => {\n      const node = buttonRef.current;\n      if (!node) return;\n\n      const rect = node.getBoundingClientRect();\n      updateOrigin(rect.width / 2, rect.height / 2);\n    }, [updateOrigin]);\n\n    const showFill = !isDisabled && (hovered || isPressed);\n\n    React.useLayoutEffect(() => {\n      const node = buttonRef.current;\n      if (!(node && showFill)) return;\n\n      const measure = () => {\n        const rect = node.getBoundingClientRect();\n        setCoverSize(\n          getCoverDiameter(rect.width, rect.height, origin.x, origin.y)\n        );\n      };\n\n      measure();\n\n      const observer = new ResizeObserver(measure);\n      observer.observe(node);\n\n      const fonts = document.fonts;\n      if (fonts?.ready) {\n        fonts.ready.then(measure).catch(() => undefined);\n      }\n\n      return () => observer.disconnect();\n    }, [showFill, origin.x, origin.y]);\n\n    const fillTransition = { duration: FILL_DURATION, ease: FILL_EASE };\n\n    const setMergedRef = React.useCallback(\n      (node: HTMLButtonElement | null) => {\n        buttonRef.current = node;\n        assignRef(ref, node);\n      },\n      [ref]\n    );\n\n    return (\n      <motion.button\n        {...props}\n        aria-busy={loading || undefined}\n        className={cn(\n          componentThemeClassName,\n          controlCornerClassName,\n          \"relative inline-flex h-12 cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden px-8 font-medium text-[15px] tracking-[-0.02em]\",\n          \"border-[0.5px] border-border bg-card text-card-foreground\",\n          \"dark:bg-muted dark:text-foreground\",\n          \"transition-[color] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]\",\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          showFill && \"text-background dark:text-neutral-950\",\n          className\n        )}\n        data-pressed={isPressed ? \"true\" : \"false\"}\n        disabled={isDisabled}\n        onBlur={(event) => {\n          onBlur?.(event);\n          setIsPressed(false);\n          if (!event.defaultPrevented) {\n            setHovered(false);\n          }\n        }}\n        onClick={onClick}\n        onFocus={(event) => {\n          onFocus?.(event);\n          if (isDisabled || event.defaultPrevented) return;\n          if (event.currentTarget.matches(\":focus-visible\")) {\n            updateOriginFromCenter();\n            setHovered(true);\n          }\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n\n          if (\n            event.defaultPrevented ||\n            isDisabled ||\n            event.repeat ||\n            (event.key !== \" \" && event.key !== \"Enter\")\n          ) {\n            return;\n          }\n\n          if (event.key === \" \") {\n            event.preventDefault();\n          }\n\n          updateOriginFromCenter();\n          setIsPressed(true);\n          setHovered(true);\n        }}\n        onKeyUp={(event) => {\n          onKeyUp?.(event);\n\n          if (event.key === \" \" || event.key === \"Enter\") {\n            setIsPressed(false);\n            if (!event.currentTarget.matches(\":focus-visible\")) {\n              setHovered(false);\n            }\n          }\n        }}\n        onPointerCancel={(event) => {\n          onPointerCancel?.(event);\n          setIsPressed(false);\n        }}\n        onPointerDown={(event) => {\n          onPointerDown?.(event);\n\n          if (event.defaultPrevented || isDisabled || event.button !== 0) {\n            return;\n          }\n\n          updateOriginFromPointer(event);\n          setIsPressed(true);\n          setHovered(true);\n        }}\n        onPointerEnter={(event) => {\n          onPointerEnter?.(event);\n          if (isDisabled || event.defaultPrevented) return;\n          updateOriginFromPointer(event);\n          setHovered(true);\n        }}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event);\n          setHovered(false);\n          setIsPressed(false);\n        }}\n        onPointerUp={(event) => {\n          onPointerUp?.(event);\n          setIsPressed(false);\n        }}\n        ref={setMergedRef}\n        type={type}\n        whileTap={isDisabled ? undefined : { scale: 0.96 }}\n      >\n        <motion.span\n          animate={{ scale: showFill && coverSize > 0 ? 1 : 0 }}\n          aria-hidden\n          className=\"pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground dark:bg-neutral-50\"\n          initial={false}\n          style={{\n            height: coverSize,\n            left: origin.x,\n            top: origin.y,\n            width: coverSize,\n          }}\n          transition={fillTransition}\n        />\n        <span className=\"relative z-10 inline-flex items-center justify-center gap-2\">\n          {children}\n        </span>\n      </motion.button>\n    );\n  }\n);\nRadialButton.displayName = \"RadialButton\";\n\nexport { RadialButton };\nexport type { RadialButtonProps };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Radial Button",
  "description": "Rounded action button with a radial foreground fill that spreads on hover or focus-visible. Built with Motion."
}
