{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "Press target that answers on pointer-down: a 3% dip in 100 ms, so the click feels acknowledged.",
  "dependencies": [
    "radix-ui",
    "class-variance-authority",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/beam-border.json",
    "https://miseui.junsu4621.workers.dev/r/liquid-metal.json",
    "https://miseui.junsu4621.workers.dev/r/shimmer-text.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/button.tsx",
      "content": "import type * as React from \"react\"\nimport { Slot } from \"radix-ui\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n  ButtonEffectFrame,\n  EFFECT_SURFACE_CLASS,\n  LIQUID_TINTED_INK,\n  shimmerLabel,\n  type ButtonEffect,\n} from \"@/registry/miseui/ui/button-effects\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst buttonVariants = cva(\n  [\n    \"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium\",\n    \"transition-[color,background-color,border-color,box-shadow,scale] duration-(--duration-fast) ease-(--ease-mise)\",\n    \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n    \"disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97] active:duration-100\",\n    \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  ].join(\" \"),\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        mise: \"bg-mise text-mise-foreground shadow-xs hover:bg-mise/90\",\n        destructive:\n          \"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/30\",\n        outline: \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n        secondary: \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n        lg: \"h-11 rounded-full px-6 text-[15px] has-[>svg]:px-5\",\n        icon: \"size-9\",\n      },\n    },\n    defaultVariants: { variant: \"default\", size: \"default\" },\n  },\n)\n\ntype ButtonProps = React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n    /** A living treatment on top of the variant. Undefined is the plain button. */\n    effect?: ButtonEffect\n  }\n\nfunction Button({ className, variant, size, asChild = false, effect, children, ...props }: ButtonProps) {\n  const Comp = asChild ? Slot.Root : \"button\"\n  const isShimmer = effect === \"shimmer\"\n  const button = (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant ?? \"default\"}\n      data-effect={effect}\n      aria-busy={isShimmer ? true : undefined}\n      className={cn(\n        buttonVariants({ variant, size }),\n        effect && EFFECT_SURFACE_CLASS[effect],\n        effect === \"liquid\" && variant === \"mise\" && LIQUID_TINTED_INK,\n        className,\n      )}\n      {...props}\n    >\n      {isShimmer ? shimmerLabel(children, asChild, variant) : children}\n    </Comp>\n  )\n  if (!effect || effect === \"shimmer\") return button\n  return (\n    <ButtonEffectFrame\n      effect={effect}\n      variant={variant}\n      size={size}\n      className={className}\n      disabled={Boolean(props.disabled)}\n    >\n      {button}\n    </ButtonEffectFrame>\n  )\n}\n\nexport { Button, buttonVariants, type ButtonProps, type ButtonEffect }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/button-effects.tsx",
      "content": "import type * as React from \"react\"\nimport { Children, cloneElement, isValidElement, useEffect, useRef } from \"react\"\nimport { Slot } from \"radix-ui\"\nimport { useMotionTemplate, useMotionValueEvent, useReducedMotion, useSpring } from \"motion/react\"\nimport { BeamBorder } from \"@/registry/miseui/ui/beam-border\"\nimport { LiquidMetal } from \"@/registry/miseui/ui/liquid-metal\"\nimport { ShimmerText } from \"@/registry/miseui/ui/shimmer-text\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ButtonEffect = \"liquid\" | \"beam\" | \"shimmer\" | \"magnetic\"\n\nconst BEAM_LENGTH_PX = 72\nconst BEAM_WIDTH_PX = 1.5\nconst BEAM_LAP_SECONDS = 3\nconst MAGNET_REACH_PX = 4\nconst MAGNET_SPRING = { bounce: 0, visualDuration: 0.35 }\nconst FINE_POINTER_QUERY = \"(hover: hover) and (pointer: fine)\"\nconst SHIMMER_REST_SHARE = \"45%\"\n/** Layout tokens that describe the visual unit, so they must also reach the frame around the button. */\nconst FRAME_TOKEN = /^(?:rounded|w-full$|flex-1$|grow$)/\n/** The frame carries the press dip when it wraps the button, or the label would sink while its surface stays. */\nconst FRAME_PRESS_CLASS = \"active:scale-100\"\n\n/**\n * Applied to the button itself, between its variant and the consumer's className.\n * Silver stays light in every theme and palette, so its ink is a fixed dark. --mise-foreground is white in\n * palettes with a deep accent, which would vanish on silver, so only the tinted metal uses it (LIQUID_TINTED_INK).\n */\nconst EFFECT_SURFACE_CLASS: Record<ButtonEffect, string> = {\n  liquid: cn(\n    \"border-transparent bg-transparent font-semibold text-[oklch(0.2_0_0)] shadow-none\",\n    \"hover:bg-transparent hover:text-[oklch(0.2_0_0)] disabled:opacity-100\",\n    FRAME_PRESS_CLASS,\n  ),\n  beam: FRAME_PRESS_CLASS,\n  // Loading is not \"unavailable\": a dimmed label would hide the one thing saying work is happening.\n  shimmer: \"disabled:opacity-100\",\n  magnetic: \"\",\n}\n\n/**\n * ShimmerText paints with --muted-foreground and --foreground on a transparent text fill, so currentColor\n * resolves to nothing inside it and each variant's ink has to be named. Outline and ghost already sit on\n * --foreground, and re-declaring a variable from itself would invalidate it, so they keep the defaults.\n */\nconst SHIMMER_INK: Partial<Record<string, string>> = {\n  default: \"var(--primary-foreground)\",\n  mise: \"var(--mise-foreground)\",\n  destructive: \"var(--color-white)\",\n  secondary: \"var(--secondary-foreground)\",\n  link: \"var(--primary)\",\n}\n\nfunction shimmerStyle(variant: string | null | undefined) {\n  const ink = SHIMMER_INK[variant ?? \"default\"]\n  if (!ink) return undefined\n  return {\n    \"--foreground\": ink,\n    \"--muted-foreground\": `color-mix(in oklab, ${ink} ${SHIMMER_REST_SHARE}, transparent)`,\n  } as React.CSSProperties\n}\n\nfunction shimmerNodes(children: React.ReactNode, style: React.CSSProperties | undefined) {\n  return Children.map(children, (child) =>\n    typeof child === \"string\" || typeof child === \"number\" ? (\n      <ShimmerText style={style}>{String(child)}</ShimmerText>\n    ) : (\n      child\n    ),\n  )\n}\n\n/** Text runs through the shimmer, icons stay as they are. With asChild the text lives one level down, in the child. */\nfunction shimmerLabel(children: React.ReactNode, asChild: boolean, variant: string | null | undefined) {\n  const style = shimmerStyle(variant)\n  if (asChild && isValidElement<{ children?: React.ReactNode }>(children)) {\n    return cloneElement(children, undefined, shimmerNodes(children.props.children, style))\n  }\n  return shimmerNodes(children, style)\n}\n\n/** -1..1 share of the half-size; a zero-size element has nowhere to pull to. */\nfunction pullShare(delta: number, half: number) {\n  if (half <= 0) return 0\n  return Math.min(1, Math.max(-1, delta / half))\n}\n\n/**\n * Writes the CSS `translate` property, which composes with the `scale` that active:scale sets, so the press\n * dip stays instant. Going through Slot means no wrapper and no motion.button, so asChild keeps working.\n */\nfunction MagneticSlot({ disabled, children }: { disabled: boolean; children: React.ReactElement }) {\n  const reduceMotion = useReducedMotion()\n  const nodeRef = useRef<HTMLElement>(null)\n  const x = useSpring(0, MAGNET_SPRING)\n  const y = useSpring(0, MAGNET_SPRING)\n  const translate = useMotionTemplate`${x}px ${y}px`\n\n  useMotionValueEvent(translate, \"change\", (value) => {\n    if (nodeRef.current) nodeRef.current.style.translate = value\n  })\n\n  // A button disabled mid-hover stops receiving pointer events, so pointerleave would never bring it home.\n  useEffect(() => {\n    if (!disabled) return\n    x.set(0)\n    y.set(0)\n  }, [disabled, x, y])\n\n  const pull = (event: React.PointerEvent<HTMLElement>) => {\n    if (reduceMotion || !window.matchMedia(FINE_POINTER_QUERY).matches) return\n    const rect = event.currentTarget.getBoundingClientRect()\n    // The rect already includes the drift; without subtracting it the button would chase its own offset.\n    const centerX = rect.left + rect.width / 2 - x.get()\n    const centerY = rect.top + rect.height / 2 - y.get()\n    x.set(pullShare(event.clientX - centerX, rect.width / 2) * MAGNET_REACH_PX)\n    y.set(pullShare(event.clientY - centerY, rect.height / 2) * MAGNET_REACH_PX)\n  }\n\n  const release = () => {\n    x.set(0)\n    y.set(0)\n  }\n\n  return (\n    <Slot.Root ref={nodeRef} onPointerMove={pull} onPointerLeave={release}>\n      {children}\n    </Slot.Root>\n  )\n}\n\ninterface ButtonEffectFrameProps {\n  effect: Exclude<ButtonEffect, \"shimmer\">\n  variant: string | null | undefined\n  size: string | null | undefined\n  className: string | undefined\n  disabled: boolean\n  children: React.ReactElement\n}\n\nfunction frameClass(size: string | null | undefined, className: string | undefined) {\n  const inherited = (className ?? \"\").split(/\\s+/).filter((token) => FRAME_TOKEN.test(token))\n  return cn(\"inline-flex w-fit has-active:scale-[0.97]\", size === \"lg\" ? \"rounded-full\" : \"rounded-md\", inherited)\n}\n\nfunction ButtonEffectFrame({ effect, variant, size, className, disabled, children }: ButtonEffectFrameProps) {\n  if (effect === \"magnetic\") return <MagneticSlot disabled={disabled}>{children}</MagneticSlot>\n  const frame = frameClass(size, className)\n  const isMise = variant === \"mise\"\n  if (effect === \"beam\") {\n    return (\n      <BeamBorder\n        active={!disabled}\n        // A warm beam on a warm surface would vanish.\n        color={isMise ? \"foreground\" : \"mise\"}\n        size={BEAM_LENGTH_PX}\n        borderWidth={BEAM_WIDTH_PX}\n        duration={BEAM_LAP_SECONDS}\n        className={frame}\n      >\n        {children}\n      </BeamBorder>\n    )\n  }\n  return (\n    <LiquidMetal\n      tint={isMise ? \"mise\" : \"silver\"}\n      speed={disabled ? 0 : undefined}\n      className={cn(\n        frame,\n        \"transition-[filter] duration-(--duration-fast) ease-(--ease-mise) has-disabled:opacity-50\",\n        !disabled && \"hover:brightness-110\",\n        // Without WebGL2 there is no canvas behind the transparent button.\n        \"data-[fallback=true]:bg-mise\",\n      )}\n    >\n      {children}\n    </LiquidMetal>\n  )\n}\n\nconst LIQUID_TINTED_INK = \"text-mise-foreground hover:text-mise-foreground\"\n\nexport { ButtonEffectFrame, EFFECT_SURFACE_CLASS, LIQUID_TINTED_INK, shimmerLabel, type ButtonEffect }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}