{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sliding-chip-button",
  "title": "Sliding Chip Button",
  "description": "A chip-style call-to-action link with a Motion-powered sliding label and rotating icon chips on hover.",
  "dependencies": ["motion", "class-variance-authority"],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/primitives/buttons/sliding-chip-button/index.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  motion,\n  type Transition,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  cloneElement,\n  type FocusEvent,\n  isValidElement,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  type RefCallback,\n  type RefObject,\n  useEffect,\n  useState,\n} from \"react\";\n\nconst DEFAULT_EASE = [0.77, 0, 0.175, 1] as const;\nconst DEFAULT_DURATION = 0.7;\n\nconst LABEL_OFFSET_BY_SIZE = {\n  sm: { default: -30, lg: -36 },\n  default: { default: -38, lg: -46 },\n  lg: { default: -44, lg: -52 },\n} as const;\n\nconst slidingChipButtonVariants = cva(\n  \"inline-flex min-w-0 shrink-0 cursor-pointer items-center justify-center whitespace-nowrap font-mono uppercase outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        accent: \"focus-visible:ring-accent-pro/50\",\n        inverted: \"focus-visible:ring-foreground/30\",\n        outline: \"focus-visible:ring-border\",\n        ghost: \"focus-visible:ring-muted-foreground/30\",\n      },\n      size: {\n        sm: \"text-xs\",\n        default: \"text-sm\",\n        lg: \"text-base\",\n      },\n    },\n    defaultVariants: {\n      variant: \"accent\",\n      size: \"default\",\n    },\n  }\n);\n\nconst slidingChipButtonChipVariants = cva(\n  \"pointer-events-none flex items-center justify-center will-change-transform\",\n  {\n    variants: {\n      variant: {\n        accent: \"bg-[rgba(251,70,13)] text-neutral-950\",\n        inverted: \"bg-foreground text-background\",\n        outline: \"border border-foreground bg-transparent text-foreground\",\n        ghost: \"bg-muted text-foreground\",\n      },\n      size: {\n        sm: \"size-7\",\n        default: \"size-8 lg:size-10\",\n        lg: \"size-10 lg:size-12\",\n      },\n    },\n    defaultVariants: {\n      variant: \"accent\",\n      size: \"default\",\n    },\n  }\n);\n\nconst slidingChipButtonLabelVariants = cva(\n  \"pointer-events-none flex items-center justify-center will-change-transform\",\n  {\n    variants: {\n      variant: {\n        accent: \"bg-[rgba(251,70,13)] text-neutral-950\",\n        inverted: \"bg-foreground text-background\",\n        outline: \"border border-foreground bg-transparent text-foreground\",\n        ghost: \"bg-muted text-foreground\",\n      },\n      size: {\n        sm: \"h-7 px-2\",\n        default: \"h-8 flex-1 px-2 lg:h-10 lg:px-3\",\n        lg: \"h-10 flex-1 px-3 lg:h-12 lg:px-4\",\n      },\n    },\n    defaultVariants: {\n      variant: \"accent\",\n      size: \"default\",\n    },\n  }\n);\n\nconst slidingChipButtonGroupVariants = cva(\n  \"pointer-events-none relative flex w-full items-center\",\n  {\n    variants: {\n      size: {\n        sm: \"gap-1\",\n        default: \"gap-1.5\",\n        lg: \"gap-2\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n);\n\ntype WithAsChild<Base extends object> =\n  | (Base & { asChild: true; children: ReactElement })\n  | (Base & { asChild?: false | undefined; children?: string });\n\ntype SlidingChipButtonSize = NonNullable<\n  VariantProps<typeof slidingChipButtonVariants>[\"size\"]\n>;\n\nfunction SlidingChipButtonPlusIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={className}\n      fill=\"none\"\n      viewBox=\"0 0 12 12\"\n    >\n      <path\n        d=\"M6 1v10M1 6h10\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"square\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> {\n  return (node) => {\n    for (const ref of refs) {\n      if (!ref) {\n        continue;\n      }\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else {\n        (ref as RefObject<T | null>).current = node;\n      }\n    }\n  };\n}\n\nfunction getLinkLabel(\n  asChild: boolean,\n  children: string | ReactElement | undefined,\n  label: string | undefined\n): string {\n  if (asChild && isValidElement(children)) {\n    const childText = (children.props as { children?: unknown }).children;\n\n    if (typeof childText === \"string\") {\n      return childText;\n    }\n\n    return label ?? \"\";\n  }\n\n  if (typeof children === \"string\") {\n    return children;\n  }\n\n  return label ?? \"\";\n}\n\nfunction useResponsiveLabelOffset(\n  size: SlidingChipButtonSize,\n  labelOffset?: number,\n  labelOffsetLg?: number\n): number {\n  const offsets = LABEL_OFFSET_BY_SIZE[size ?? \"default\"];\n  const baseOffset = labelOffset ?? offsets.default;\n  const largeOffset = labelOffsetLg ?? offsets.lg;\n  const [offset, setOffset] = useState(baseOffset);\n\n  useEffect(() => {\n    if (labelOffsetLg === undefined && labelOffset !== undefined) {\n      setOffset(labelOffset);\n      return;\n    }\n\n    const mediaQuery = window.matchMedia(\"(min-width: 1024px)\");\n    const update = () => {\n      setOffset(mediaQuery.matches ? largeOffset : baseOffset);\n    };\n\n    update();\n    mediaQuery.addEventListener(\"change\", update);\n    return () => {\n      mediaQuery.removeEventListener(\"change\", update);\n    };\n  }, [baseOffset, largeOffset, labelOffset, labelOffsetLg]);\n\n  return offset;\n}\n\nfunction resolveMotionTransition(\n  shouldReduceMotion: boolean | null,\n  duration: number,\n  ease: readonly [number, number, number, number]\n): Transition {\n  if (shouldReduceMotion) {\n    return { duration: 0 };\n  }\n\n  return { duration, ease };\n}\n\ntype SlidingChipButtonBaseProps = Omit<\n  ComponentPropsWithoutRef<\"a\">,\n  \"children\"\n> &\n  VariantProps<typeof slidingChipButtonVariants> & {\n    /** Alternative to string `children` for demos and controlled previews. */\n    label?: string;\n    /**\n     * Animation duration in seconds.\n     * @default 0.7\n     */\n    duration?: number;\n    /**\n     * Cubic-bezier easing values.\n     * @default [0.77, 0, 0.175, 1]\n     */\n    ease?: readonly [number, number, number, number];\n    /** Label x-offset at rest (mobile / default breakpoint). */\n    labelOffset?: number;\n    /** Label x-offset at rest on `lg` breakpoint and up. */\n    labelOffsetLg?: number;\n    /** Rest rotation for the leading chip (degrees). @default -45 */\n    startChipRestRotate?: number;\n    /** Active rotation for the leading chip (degrees). @default 0 */\n    startChipActiveRotate?: number;\n    /** Rest rotation for the trailing chip (degrees). @default 0 */\n    endChipRestRotate?: number;\n    /** Active rotation for the trailing chip (degrees). @default -45 */\n    endChipActiveRotate?: number;\n    /** Rest scale for both chips. @default 0 for start, 1 for end */\n    startChipRestScale?: number;\n    startChipActiveScale?: number;\n    endChipRestScale?: number;\n    endChipActiveScale?: number;\n    /** Custom icon for both chips. */\n    icon?: ReactNode;\n    /** Override the leading chip icon. */\n    startIcon?: ReactNode;\n    /** Override the trailing chip icon. */\n    endIcon?: ReactNode;\n    /** Class names merged onto the motion group wrapper. */\n    groupClassName?: string;\n    /** Class names merged onto both chips and the label chip. */\n    chipClassName?: string;\n    /** Class names merged onto the label chip only. */\n    labelClassName?: string;\n    /** Class names merged onto the leading chip only. */\n    startChipClassName?: string;\n    /** Class names merged onto the trailing chip only. */\n    endChipClassName?: string;\n    /** Class names merged onto chip icons. */\n    iconClassName?: string;\n  };\n\nexport type SlidingChipButtonProps = WithAsChild<SlidingChipButtonBaseProps>;\n\ninterface SlidingChipButtonAnimatedContentProps {\n  chipClassName?: string;\n  duration: number;\n  ease: readonly [number, number, number, number];\n  endChipActiveRotate: number;\n  endChipActiveScale: number;\n  endChipClassName?: string;\n  endChipRestRotate: number;\n  endChipRestScale: number;\n  endIcon?: ReactNode;\n  groupClassName?: string;\n  icon?: ReactNode;\n  iconClassName?: string;\n  isActive: boolean;\n  label: string;\n  labelClassName?: string;\n  labelOffset: number;\n  size: SlidingChipButtonSize;\n  startChipActiveRotate: number;\n  startChipActiveScale: number;\n  startChipClassName?: string;\n  startChipRestRotate: number;\n  startChipRestScale: number;\n  startIcon?: ReactNode;\n  variant: NonNullable<SlidingChipButtonProps[\"variant\"]>;\n}\n\nfunction SlidingChipButtonAnimatedContent({\n  variant = \"accent\",\n  size = \"default\",\n  label,\n  labelOffset,\n  isActive,\n  duration,\n  ease,\n  icon,\n  startIcon,\n  endIcon,\n  groupClassName,\n  chipClassName,\n  labelClassName,\n  startChipClassName,\n  endChipClassName,\n  iconClassName,\n  startChipRestRotate,\n  startChipActiveRotate,\n  startChipRestScale,\n  startChipActiveScale,\n  endChipRestRotate,\n  endChipActiveRotate,\n  endChipRestScale,\n  endChipActiveScale,\n}: SlidingChipButtonAnimatedContentProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const motionTransition = resolveMotionTransition(\n    shouldReduceMotion,\n    duration,\n    ease\n  );\n\n  const resolvedStartIcon = startIcon ?? icon ?? (\n    <SlidingChipButtonPlusIcon className={cn(\"size-[0.75em]\", iconClassName)} />\n  );\n  const resolvedEndIcon = endIcon ?? icon ?? (\n    <SlidingChipButtonPlusIcon className={cn(\"size-[0.75em]\", iconClassName)} />\n  );\n\n  const startChipVariants: Variants = {\n    rest: { rotate: startChipRestRotate, scale: startChipRestScale },\n    hover: { rotate: startChipActiveRotate, scale: startChipActiveScale },\n  };\n\n  const endChipVariants: Variants = {\n    rest: { rotate: endChipRestRotate, scale: endChipRestScale },\n    hover: { rotate: endChipActiveRotate, scale: endChipActiveScale },\n  };\n\n  return (\n    <motion.span\n      animate={isActive ? \"hover\" : \"rest\"}\n      className={cn(slidingChipButtonGroupVariants({ size }), groupClassName)}\n      initial={false}\n    >\n      <motion.span\n        className={cn(\n          slidingChipButtonChipVariants({ variant, size }),\n          chipClassName,\n          startChipClassName\n        )}\n        style={{ transformOrigin: \"left center\" }}\n        transition={motionTransition}\n        variants={startChipVariants}\n      >\n        {resolvedStartIcon}\n      </motion.span>\n\n      <motion.span\n        animate={isActive ? { x: 0 } : { x: labelOffset }}\n        className={cn(\n          slidingChipButtonLabelVariants({ variant, size }),\n          chipClassName,\n          labelClassName\n        )}\n        initial={false}\n        transition={motionTransition}\n      >\n        {label}\n      </motion.span>\n\n      <motion.span\n        className={cn(\n          slidingChipButtonChipVariants({ variant, size }),\n          \"absolute right-0 z-10\",\n          chipClassName,\n          endChipClassName\n        )}\n        style={{ transformOrigin: \"right center\" }}\n        transition={motionTransition}\n        variants={endChipVariants}\n      >\n        {resolvedEndIcon}\n      </motion.span>\n    </motion.span>\n  );\n}\n\nfunction SlidingChipButton({\n  asChild = false,\n  variant = \"accent\",\n  size = \"default\",\n  duration = DEFAULT_DURATION,\n  ease = DEFAULT_EASE,\n  label,\n  children,\n  className,\n  groupClassName,\n  chipClassName,\n  labelClassName,\n  startChipClassName,\n  endChipClassName,\n  iconClassName,\n  labelOffset,\n  labelOffsetLg,\n  startChipRestRotate = -45,\n  startChipActiveRotate = 0,\n  startChipRestScale = 0,\n  startChipActiveScale = 1,\n  endChipRestRotate = 0,\n  endChipActiveRotate = -45,\n  endChipRestScale = 1,\n  endChipActiveScale = 0,\n  icon,\n  startIcon,\n  endIcon,\n  href,\n  ref,\n  \"aria-label\": ariaLabel,\n  onBlur,\n  onFocus,\n  onMouseEnter,\n  onMouseLeave,\n  ...props\n}: SlidingChipButtonProps & {\n  ref?: Ref<HTMLAnchorElement>;\n}) {\n  const text = getLinkLabel(asChild, children, label);\n  const resolvedSize = size ?? \"default\";\n  const resolvedLabelOffset = useResponsiveLabelOffset(\n    resolvedSize,\n    labelOffset,\n    labelOffsetLg\n  );\n  const [isActive, setIsActive] = useState(false);\n  const rootClassName = cn(\n    slidingChipButtonVariants({ variant, size, className })\n  );\n\n  const animatedContent = (\n    <SlidingChipButtonAnimatedContent\n      chipClassName={chipClassName}\n      duration={duration}\n      ease={ease}\n      endChipActiveRotate={endChipActiveRotate}\n      endChipActiveScale={endChipActiveScale}\n      endChipClassName={endChipClassName}\n      endChipRestRotate={endChipRestRotate}\n      endChipRestScale={endChipRestScale}\n      endIcon={endIcon}\n      groupClassName={groupClassName}\n      icon={icon}\n      iconClassName={iconClassName}\n      isActive={isActive}\n      label={text}\n      labelClassName={labelClassName}\n      labelOffset={resolvedLabelOffset}\n      size={resolvedSize}\n      startChipActiveRotate={startChipActiveRotate}\n      startChipActiveScale={startChipActiveScale}\n      startChipClassName={startChipClassName}\n      startChipRestRotate={startChipRestRotate}\n      startChipRestScale={startChipRestScale}\n      startIcon={startIcon}\n      variant={variant ?? \"accent\"}\n    />\n  );\n\n  const interactionHandlers = {\n    onBlur: (event: FocusEvent<HTMLElement>) => {\n      onBlur?.(event as FocusEvent<HTMLAnchorElement>);\n      setIsActive(false);\n    },\n    onFocus: (event: FocusEvent<HTMLElement>) => {\n      onFocus?.(event as FocusEvent<HTMLAnchorElement>);\n      setIsActive(true);\n    },\n    onMouseEnter: (event: MouseEvent<HTMLElement>) => {\n      onMouseEnter?.(event as MouseEvent<HTMLAnchorElement>);\n      setIsActive(true);\n    },\n    onMouseLeave: (event: MouseEvent<HTMLElement>) => {\n      onMouseLeave?.(event as MouseEvent<HTMLAnchorElement>);\n      setIsActive(false);\n    },\n  };\n\n  if (asChild) {\n    if (!isValidElement(children)) {\n      return null;\n    }\n\n    const {\n      ref: childRef,\n      className: childClassName,\n      \"aria-label\": childAriaLabel,\n      onBlur: childOnBlur,\n      onFocus: childOnFocus,\n      onMouseEnter: childOnMouseEnter,\n      onMouseLeave: childOnMouseLeave,\n      ...childProps\n    } = children.props as {\n      ref?: Ref<HTMLElement>;\n      className?: string;\n      \"aria-label\"?: string;\n      onBlur?: (event: FocusEvent<HTMLElement>) => void;\n      onFocus?: (event: FocusEvent<HTMLElement>) => void;\n      onMouseEnter?: (event: MouseEvent<HTMLElement>) => void;\n      onMouseLeave?: (event: MouseEvent<HTMLElement>) => void;\n    };\n\n    return cloneElement(\n      children,\n      {\n        ...childProps,\n        ...props,\n        ...interactionHandlers,\n        \"aria-label\": ariaLabel ?? childAriaLabel ?? (text || undefined),\n        className: cn(childClassName, rootClassName),\n        onBlur: (event: FocusEvent<HTMLElement>) => {\n          childOnBlur?.(event);\n          interactionHandlers.onBlur(event);\n        },\n        onFocus: (event: FocusEvent<HTMLElement>) => {\n          childOnFocus?.(event);\n          interactionHandlers.onFocus(event);\n        },\n        onMouseEnter: (event: MouseEvent<HTMLElement>) => {\n          childOnMouseEnter?.(event);\n          interactionHandlers.onMouseEnter(event);\n        },\n        onMouseLeave: (event: MouseEvent<HTMLElement>) => {\n          childOnMouseLeave?.(event);\n          interactionHandlers.onMouseLeave(event);\n        },\n        ref: mergeRefs(childRef, ref),\n      } as Record<string, unknown>,\n      animatedContent\n    );\n  }\n\n  return (\n    <a\n      aria-label={ariaLabel ?? (text || undefined)}\n      className={rootClassName}\n      href={href}\n      ref={ref}\n      {...interactionHandlers}\n      {...props}\n    >\n      {animatedContent}\n    </a>\n  );\n}\n\nexport {\n  SlidingChipButton,\n  SlidingChipButtonPlusIcon,\n  slidingChipButtonChipVariants,\n  slidingChipButtonGroupVariants,\n  slidingChipButtonLabelVariants,\n  slidingChipButtonVariants,\n};\n\nexport type SlidingChipButtonVariant = NonNullable<\n  SlidingChipButtonProps[\"variant\"]\n>;\n",
      "type": "registry:ui",
      "target": "components/sora-ui/buttons/sliding-chip-button.tsx"
    }
  ],
  "meta": {
    "inspiration": {
      "type": "inspired",
      "label": "Annnimate",
      "url": "https://www.annnimate.com"
    },
    "demoProps": {
      "SlidingChipButton": {
        "label": {
          "value": "Explore components"
        },
        "variant": {
          "value": "accent",
          "options": {
            "Accent": "accent",
            "Inverted": "inverted",
            "Outline": "outline",
            "Ghost": "ghost"
          }
        },
        "size": {
          "value": "default",
          "options": {
            "Small": "sm",
            "Default": "default",
            "Large": "lg"
          }
        },
        "duration": {
          "value": 0.7,
          "min": 0.2,
          "max": 2,
          "step": 0.1
        }
      }
    }
  },
  "type": "registry:ui"
}
