'use client';/** * @title Reaction Button * @description 사용자가 콘텐츠에 대한 반응을 표현할 수 있게 해주는 컴포넌트예요. 좋아요, 도움돼요 같은 감정적 피드백을 간편하게 남길 때 사용해요. */import * as React from 'react';import { Toggle as TogglePrimitive } from 'radix-ui';import { cva, type VariantProps } from 'class-variance-authority';import { cn } from '@/lib/utils';// SEED Rootage spec: reaction-button.yaml// 선택 상태(pressed)는 Radix Toggle 이 붙이는 data-state="on" 으로 표현해요.const reactionButtonVariants = cva( [ 'relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap align-middle font-medium antialiased', 'transition-[background-color,box-shadow,color,scale,outline-color] duration-(--rb-duration-color-transition) ease-easing', 'outline-2 outline-offset-2 outline-transparent focus-visible:outline-stroke-focus-ring', 'enabled:active:scale-[0.97] data-[loading]:active:scale-100', // enabled 'bg-bg-transparent text-fg-neutral inset-ring inset-ring-stroke-neutral-muted enabled:active:bg-bg-transparent-pressed', '[--track-color:var(--rb-color-palette-gray-500)] [--range-color:var(--rb-color-fg-neutral)]', // selected 'data-[state=on]:bg-bg-transparent data-[state=on]:text-fg-brand data-[state=on]:inset-ring-stroke-brand-weak', 'data-[state=on]:enabled:active:bg-bg-transparent-pressed', // loading 'data-[loading]:bg-bg-neutral-weak-pressed data-[state=on]:data-[loading]:bg-bg-transparent-pressed', 'data-[state=on]:data-[loading]:[--track-color:var(--rb-color-stroke-brand-weak)] data-[state=on]:data-[loading]:[--range-color:var(--rb-color-fg-brand)]', // disabled 'disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:text-fg-disabled disabled:inset-ring-0', 'data-[state=on]:disabled:bg-bg-disabled data-[state=on]:disabled:text-fg-disabled', '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-[18px]', ], { variants: { size: { xsmall: 'h-x8 gap-x1 rounded-full px-x3 py-x1_5 text-t3', small: 'h-x9 gap-x1 rounded-full px-x3_5 py-x2 text-t3', }, }, defaultVariants: { size: 'small', }, },);function ReactionButtonSpinner() { // progressCircle: size 14px, thickness 2px return ( <svg className="size-[14px] animate-spin" viewBox="0 0 16 16" fill="none" aria-hidden> <circle cx="8" cy="8" r="7" className="stroke-(--track-color)" strokeWidth="2" /> <path d="M8 1a7 7 0 0 1 7 7" className="stroke-(--range-color)" strokeWidth="2" strokeLinecap="round" /> </svg> );}export interface ReactionButtonProps extends React.ComponentProps<typeof TogglePrimitive.Root>, VariantProps<typeof reactionButtonVariants> { /** 로딩 중에는 내용을 숨기고 스피너를 보여주며 클릭을 막아요. */ loading?: boolean;}function ReactionButton({ className, size, loading = false, asChild = false, children, ...props }: ReactionButtonProps) { return ( <TogglePrimitive.Root data-slot="reaction-button" data-loading={loading || undefined} aria-busy={loading || undefined} asChild={asChild} className={cn(reactionButtonVariants({ size }), loading && 'pointer-events-none', className)} {...props} > {asChild ? ( children ) : ( <> <span className={cn('inline-flex items-center gap-[inherit]', loading && 'invisible')}>{children}</span> {loading && ( <span className="absolute inset-0 flex items-center justify-center"> <ReactionButtonSpinner /> </span> )} </> )} </TogglePrimitive.Root> );}/** 반응 수를 굵게 표시하는 슬롯이에요. */function ReactionButtonCount({ className, ...props }: React.ComponentProps<'span'>) { return ( <span data-slot="reaction-button-count" className={cn('font-bold tabular-nums', className)} {...props} /> );}export { ReactionButton, ReactionButtonCount, reactionButtonVariants };