{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "border-beam-button",
  "type": "registry:ui",
  "title": "Border Beam Button",
  "description": "A button whose hairline border carries a travelling two-colour beam that brightens on hover and focus.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/border-beam-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport interface BorderBeamButtonProps extends Omit<React.HTMLAttributes<HTMLElement>, \"color\"> {\n  /** Button label. */\n  children?: React.ReactNode;\n  /** Renders an `<a>` instead of a `<button>`. */\n  href?: string;\n  target?: React.HTMLAttributeAnchorTarget;\n  rel?: string;\n  /** Native button type (ignored for links). */\n  type?: \"button\" | \"submit\" | \"reset\";\n  disabled?: boolean;\n  /** Filled dark surface or a quiet outlined surface. */\n  variant?: \"solid\" | \"ghost\";\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Corner style. The beam follows the same radius. */\n  shape?: \"pill\" | \"rounded\" | \"square\";\n  /** Time for one lap around the border, in ms. */\n  duration?: number;\n  /** Length of the beam in px. */\n  length?: number;\n  /** Colour at the head of the beam. */\n  colorFrom?: string;\n  /** Colour the beam fades through towards its tail. */\n  colorTo?: string;\n  /** Run the beam all the time, or only while hovered, focused or pressed. */\n  trigger?: \"always\" | \"hover\";\n}\n\nconst SIZES = {\n  sm: \"h-9 gap-1.5 px-4 text-[13px]\",\n  md: \"h-11 gap-2 px-5 text-sm\",\n  lg: \"h-13 gap-2.5 px-7 text-[15px]\",\n} as const;\n\nconst RADII = { pill: 9999, rounded: 12, square: 4 } as const;\n\nconst VARIANTS = {\n  solid:\n    \"border-zinc-950 bg-zinc-950 text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.08)] hover:bg-zinc-900 dark:border-white/10 dark:bg-[#0b0d0f] dark:hover:bg-black\",\n  ghost:\n    \"border-zinc-900/15 bg-white/50 text-zinc-900 hover:bg-white/80 dark:border-white/12 dark:bg-white/[0.03] dark:text-zinc-100 dark:hover:bg-white/[0.06]\",\n} as const;\n\n/** Shows only the 1px ring between the padding box and the border box (classic mask \"xor\"). */\nconst RING_MASK: React.CSSProperties = {\n  WebkitMask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n  WebkitMaskComposite: \"xor\",\n  mask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n  maskComposite: \"exclude\",\n};\n\nfunction subscribeReducedMotion(cb: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", cb);\n  return () => mq.removeEventListener(\"change\", cb);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nconst noopSubscribe = () => () => {};\n\n/** `offset-path: rect()` moves the beam at constant speed along a rounded rectangle. */\nfunction useOffsetPathSupport() {\n  return React.useSyncExternalStore(\n    noopSubscribe,\n    () => typeof CSS !== \"undefined\" && CSS.supports(\"offset-path\", \"rect(0 auto auto 0 round 4px)\"),\n    () => false,\n  );\n}\n\n/**\n * A button (or link) whose 1px border carries a travelling beam of light. The beam is a small\n * gradient square moved along `offset-path: rect()` with the Web Animations API and clipped to\n * the border ring by a mask, so it travels at constant speed and turns every corner.\n */\nexport const BorderBeamButton = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, BorderBeamButtonProps>(\n  function BorderBeamButton(\n    {\n      children = \"Start building\",\n      href,\n      target,\n      rel,\n      type = \"button\",\n      disabled = false,\n      variant = \"solid\",\n      size = \"md\",\n      shape = \"pill\",\n      duration = 3000,\n      length = 80,\n      colorFrom = \"#FF7448\",\n      colorTo = \"#B6F3D1\",\n      trigger = \"always\",\n      className = \"\",\n      style,\n      onPointerEnter,\n      onPointerLeave,\n      onPointerDown,\n      onPointerUp,\n      onPointerCancel,\n      onFocus,\n      onBlur,\n      ...props\n    },\n    ref,\n  ) {\n    const reduceMotion = usePrefersReducedMotion();\n    const supportsPath = useOffsetPathSupport();\n    const animated = supportsPath && !reduceMotion;\n\n    const [hovered, setHovered] = React.useState(false);\n    const [focused, setFocused] = React.useState(false);\n    const [pressed, setPressed] = React.useState(false);\n    const engaged = !disabled && (hovered || focused || pressed);\n\n    const frameRef = React.useRef<HTMLSpanElement>(null);\n    const beamRef = React.useRef<HTMLSpanElement>(null);\n    const glowRef = React.useRef<HTMLSpanElement>(null);\n    const animsRef = React.useRef<Animation[]>([]);\n    const stateRef = React.useRef({ visible: true, engaged: false, trigger });\n    const syncRef = React.useRef<() => void>(() => {});\n\n    // Create the looping animations once per duration; play state is handled by `sync`.\n    React.useEffect(() => {\n      const frame = frameRef.current;\n      const nodes = [beamRef.current, glowRef.current].filter((n): n is HTMLSpanElement => n !== null);\n      if (!animated || !frame || nodes.length === 0 || typeof frame.animate !== \"function\") return;\n\n      const anims = nodes.map((node) =>\n        node.animate([{ offsetDistance: \"0%\" }, { offsetDistance: \"100%\" }], {\n          duration: Math.max(400, duration),\n          iterations: Infinity,\n          easing: \"linear\",\n        }),\n      );\n      animsRef.current = anims;\n      let pauseTimer: ReturnType<typeof setTimeout> | undefined;\n\n      const sync = () => {\n        const { visible, engaged: on, trigger: mode } = stateRef.current;\n        const wanted = visible && !document.hidden && (mode === \"always\" || on);\n        clearTimeout(pauseTimer);\n        for (const a of anims) a.updatePlaybackRate(on ? 1.6 : 1);\n        if (wanted) {\n          for (const a of anims) if (a.playState !== \"running\") a.play();\n        } else if (mode === \"hover\" && visible && !document.hidden) {\n          // Let the beam keep moving while it fades out, then stop spending frames on it.\n          pauseTimer = setTimeout(() => anims.forEach((a) => a.pause()), 500);\n        } else {\n          for (const a of anims) a.pause();\n        }\n      };\n      syncRef.current = sync;\n\n      const io = new IntersectionObserver(([entry]) => {\n        stateRef.current.visible = entry.isIntersecting;\n        sync();\n      });\n      io.observe(frame);\n      document.addEventListener(\"visibilitychange\", sync);\n      sync();\n\n      return () => {\n        clearTimeout(pauseTimer);\n        io.disconnect();\n        document.removeEventListener(\"visibilitychange\", sync);\n        anims.forEach((a) => a.cancel());\n        animsRef.current = [];\n        syncRef.current = () => {};\n      };\n    }, [animated, duration]);\n\n    React.useEffect(() => {\n      stateRef.current.engaged = engaged;\n      stateRef.current.trigger = trigger;\n      syncRef.current();\n    }, [engaged, trigger]);\n\n    const radius = RADII[shape] ?? RADII.pill;\n    const beamSize = Math.max(16, length);\n    const haloRadius = radius === 9999 ? radius : radius + 16;\n    const beamStyle: React.CSSProperties = {\n      width: beamSize,\n      height: beamSize,\n      offsetPath: `rect(0 auto auto 0 round ${radius}px)`,\n      offsetAnchor: \"50% 50%\",\n      // Head (front, in the direction of travel) → second colour → transparent tail.\n      background: \"linear-gradient(to left, var(--bb-from), var(--bb-to) 45%, transparent)\",\n    };\n\n    const shared = {\n      \"data-engaged\": engaged ? \"true\" : \"false\",\n      \"data-variant\": variant,\n      className: `group/beam relative isolate inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap border font-medium tracking-tight transition-[background-color,color,border-color,scale] duration-200 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--bb-from)] active:scale-[0.98] motion-reduce:active:scale-100 ${SIZES[size] ?? SIZES.md} ${VARIANTS[variant] ?? VARIANTS.solid} ${disabled ? \"pointer-events-none opacity-50\" : \"cursor-pointer\"} ${className}`,\n      style: {\n        [\"--bb-from\" as string]: colorFrom,\n        [\"--bb-to\" as string]: colorTo,\n        borderRadius: radius,\n        ...style,\n      } as React.CSSProperties,\n      onPointerEnter: (event: React.PointerEvent<HTMLElement>) => {\n        onPointerEnter?.(event);\n        if (event.pointerType === \"mouse\") setHovered(true);\n      },\n      onPointerLeave: (event: React.PointerEvent<HTMLElement>) => {\n        onPointerLeave?.(event);\n        setHovered(false);\n        setPressed(false);\n      },\n      // Touch has no hover: light the beam while the finger is down instead.\n      onPointerDown: (event: React.PointerEvent<HTMLElement>) => {\n        onPointerDown?.(event);\n        if (event.pointerType !== \"mouse\") setPressed(true);\n      },\n      onPointerUp: (event: React.PointerEvent<HTMLElement>) => {\n        onPointerUp?.(event);\n        setPressed(false);\n      },\n      onPointerCancel: (event: React.PointerEvent<HTMLElement>) => {\n        onPointerCancel?.(event);\n        setPressed(false);\n      },\n      onFocus: (event: React.FocusEvent<HTMLElement>) => {\n        onFocus?.(event);\n        if (event.currentTarget.matches(\":focus-visible\")) setFocused(true);\n      },\n      onBlur: (event: React.FocusEvent<HTMLElement>) => {\n        onBlur?.(event);\n        setFocused(false);\n      },\n    };\n\n    // At rest the beam is dimmer (\"always\") or hidden (\"hover\"); engaging brightens it and adds a halo.\n    const restOpacity = trigger === \"always\" ? \"opacity-80\" : \"opacity-0\";\n\n    const content = (\n      <>\n        {/* Halo: a blurred copy of the beam, masked to a band just outside the border. */}\n        {animated && (\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute -inset-[17px] rounded-[inherit] p-4 opacity-0 transition-opacity duration-500 group-data-[engaged=true]/beam:opacity-60\"\n            style={{ ...RING_MASK, borderRadius: haloRadius }}\n          >\n            <span\n              ref={glowRef}\n              className=\"absolute left-0 top-0 blur-[6px]\"\n              // Same path as the crisp beam (inset back to the button edge), so the halo stays in step.\n              style={{ ...beamStyle, width: beamSize, height: 14, offsetPath: `inset(16px round ${radius}px)` }}\n            />\n          </span>\n        )}\n        {/* The 1px ring that carries the beam (or a static gradient with reduced motion). */}\n        <span\n          ref={frameRef}\n          aria-hidden=\"true\"\n          className={`pointer-events-none absolute -inset-px rounded-[inherit] p-px transition-opacity duration-300 group-data-[engaged=true]/beam:opacity-100 ${restOpacity}`}\n          style={{\n            ...RING_MASK,\n            background: animated\n              ? undefined\n              : \"linear-gradient(115deg, var(--bb-from), var(--bb-to) 32%, transparent 58%, transparent 74%, color-mix(in oklab, var(--bb-from) 60%, transparent))\",\n          }}\n        >\n          {animated && <span ref={beamRef} className=\"absolute left-0 top-0\" style={beamStyle} />}\n        </span>\n        <span className=\"relative inline-flex items-center gap-[inherit]\">{children}</span>\n      </>\n    );\n\n    if (href !== undefined) {\n      return (\n        <a\n          ref={ref as React.Ref<HTMLAnchorElement>}\n          href={disabled ? undefined : href}\n          target={target}\n          rel={rel ?? (target === \"_blank\" ? \"noreferrer\" : undefined)}\n          aria-disabled={disabled || undefined}\n          {...shared}\n          {...props}\n        >\n          {content}\n        </a>\n      );\n    }\n\n    return (\n      <button ref={ref as React.Ref<HTMLButtonElement>} type={type} disabled={disabled} {...shared} {...props}>\n        {content}\n      </button>\n    );\n  },\n);\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "buttons"
  ],
  "docs": "Documentation: https://wancraft.my/components/border-beam-button"
}
