{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-trail-button",
  "type": "registry:ui",
  "title": "Icon Trail Button",
  "description": "A pill button whose icon pill expands on hover or focus, playing out a staggered chevron trail before it parks. CSS transitions and a small state machine — no animation library.",
  "dependencies": [],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/icon-trail-button.tsx",
      "type": "registry:component",
      "target": "components/tweenui/icon-trail-button.tsx",
      "content": "'use client';\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type Ref,\n  type SVGProps,\n} from 'react';\nimport { cn } from '@/lib/utils';\n\n/* -------------------------------------------------------------------------- */\n/*  Icon — a custom \"stacked chevron\" glyph (diagonal climb of squares).       */\n/* -------------------------------------------------------------------------- */\nfunction StackedChevronIcon(props: SVGProps<SVGSVGElement>) {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" {...props}>\n      <path d=\"M8 5H10V7H8V5Z\" />\n      <path d=\"M11 8H13V10H11V8Z\" />\n      <path d=\"M14 11H16V13H14V11Z\" />\n      <path d=\"M11 14H13V16H11V14Z\" />\n      <path d=\"M8 17H10V19H8V17Z\" />\n    </svg>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Interaction — a tiny state machine driving `data-icon-state`.              */\n/*  CSS does the motion; this only decides idle → expand → park.               */\n/* -------------------------------------------------------------------------- */\ntype IconSlideState = 'idle' | 'expand' | 'park';\ntype IconSlideDir = 'in' | 'out';\n\nconst EXPAND_MS = 340;\nconst PARK_MS = 260;\n\nconst canHover = () =>\n  typeof window !== 'undefined' && window.matchMedia('(hover: hover) and (pointer: fine)').matches;\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst isButtonOn = (el: HTMLElement) =>\n  !el.matches('[disabled]') &&\n  (el.matches(':focus-visible') || (canHover() && el.matches(':hover')));\n\nfunction useButtonIconSlide() {\n  const [iconState, setIconState] = useState<IconSlideState>('idle');\n  const dirRef = useRef<IconSlideDir | null>(null);\n  const timerRef = useRef<number | null>(null);\n  const stateRef = useRef<IconSlideState>('idle');\n  const buttonRef = useRef<HTMLElement | null>(null);\n\n  const clearTimer = useCallback(() => {\n    if (timerRef.current !== null) {\n      window.clearTimeout(timerRef.current);\n      timerRef.current = null;\n    }\n  }, []);\n\n  const setState = useCallback((next: IconSlideState) => {\n    stateRef.current = next;\n    setIconState(next);\n  }, []);\n\n  const wait = useCallback(\n    (ms: number, fn: () => void) => {\n      clearTimer();\n      timerRef.current = window.setTimeout(() => {\n        timerRef.current = null;\n        fn();\n      }, ms);\n    },\n    [clearTimer]\n  );\n\n  useEffect(() => {\n    const btn = buttonRef.current;\n    if (!btn) return;\n\n    const step = () => {\n      const on = isButtonOn(btn);\n\n      if (prefersReducedMotion()) {\n        clearTimer();\n        setState(on ? 'park' : 'idle');\n        return;\n      }\n\n      if (on) {\n        if (stateRef.current === 'park') return;\n        if (stateRef.current === 'idle') {\n          setState('expand');\n          dirRef.current = 'in';\n          wait(EXPAND_MS, step);\n          return;\n        }\n        if (dirRef.current === 'in' && timerRef.current) return;\n        clearTimer();\n        setState('park');\n        dirRef.current = 'in';\n        return;\n      }\n\n      if (stateRef.current === 'idle') return;\n      if (stateRef.current === 'park') {\n        setState('expand');\n        dirRef.current = 'out';\n        wait(PARK_MS, step);\n        return;\n      }\n      if (dirRef.current === 'out' && timerRef.current) return;\n      clearTimer();\n      setState('idle');\n      dirRef.current = 'out';\n    };\n\n    const sync = () => step();\n    btn.addEventListener('pointerenter', sync);\n    btn.addEventListener('pointerleave', sync);\n    btn.addEventListener('focus', sync);\n    btn.addEventListener('blur', sync);\n\n    return () => {\n      clearTimer();\n      btn.removeEventListener('pointerenter', sync);\n      btn.removeEventListener('pointerleave', sync);\n      btn.removeEventListener('focus', sync);\n      btn.removeEventListener('blur', sync);\n    };\n  }, [clearTimer, setState, wait]);\n\n  return { iconState, buttonRef };\n}\n\n/* -------------------------------------------------------------------------- */\n/*  The staggered chevron trail that plays while the pill expands.             */\n/* -------------------------------------------------------------------------- */\nconst TRAIL_OPACITIES = [1, 0.8, 0.6, 0.4, 0.2] as const;\n\nfunction ButtonIconSlideTrail({ iconClassName }: { iconClassName: string }) {\n  return (\n    <>\n      <span\n        className=\"absolute inset-0 grid place-items-center opacity-100 transition-opacity duration-[260ms] ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[icon-state=expand]:opacity-0 motion-reduce:transition-none\"\n        aria-hidden=\"true\"\n      >\n        <StackedChevronIcon className={cn('size-6', iconClassName)} />\n      </span>\n      <span className=\"absolute inset-0 flex items-center justify-around px-3\" aria-hidden=\"true\">\n        {TRAIL_OPACITIES.map((opacity, index) => (\n          <span\n            key={opacity}\n            className={cn(\n              'size-6 -translate-x-[6px] opacity-0 transition-[opacity,translate] delay-0 duration-[260ms] ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[icon-state=expand]:translate-x-0 group-data-[icon-state=expand]:opacity-100 group-data-[icon-state=park]:translate-x-[6px] group-data-[icon-state=park]:opacity-0 motion-reduce:!-translate-x-[6px] motion-reduce:transition-none motion-reduce:!delay-0',\n              index === 0 && 'group-data-[icon-state=expand]:delay-[40ms]',\n              index === 1 && 'group-data-[icon-state=expand]:delay-[65ms]',\n              index === 2 && 'group-data-[icon-state=expand]:delay-[90ms]',\n              index === 3 && 'group-data-[icon-state=expand]:delay-[115ms]',\n              index === 4 && 'group-data-[icon-state=expand]:delay-[140ms]'\n            )}\n          >\n            <StackedChevronIcon className={cn('size-full', iconClassName)} style={{ opacity }} />\n          </span>\n        ))}\n      </span>\n    </>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Icon Trail Button — colors, easing and sizes are inline, so the      */\n/*  component is self-contained and needs no Tailwind @theme tokens.           */\n/* -------------------------------------------------------------------------- */\ninterface IconTrailButtonOwnProps {\n  /** Extra classes for the label span. */\n  labelClassName?: string;\n}\n\n/** Without `href` it is a button; with one it renders an anchor instead. */\nexport type IconTrailButtonProps =\n  | (IconTrailButtonOwnProps & ComponentPropsWithoutRef<'button'> & { href?: never })\n  | (IconTrailButtonOwnProps & ComponentPropsWithoutRef<'a'> & { href: string });\n\nexport default function IconTrailButton({\n  children,\n  labelClassName,\n  className,\n  ...props\n}: IconTrailButtonProps) {\n  const { iconState, buttonRef } = useButtonIconSlide();\n  const isLink = typeof props.href === 'string';\n  const Tag = isLink ? 'a' : 'button';\n  const tagProps = isLink\n    ? props\n    : { type: 'button' as const, ...(props as ComponentPropsWithoutRef<'button'>) };\n\n  return (\n    <Tag\n      {...(tagProps as ComponentPropsWithoutRef<'button'> & ComponentPropsWithoutRef<'a'>)}\n      ref={buttonRef as Ref<HTMLButtonElement & HTMLAnchorElement>}\n      data-btn-icon-slide\n      data-icon-state={iconState}\n      className={cn(\n        'group relative inline-flex h-12 origin-center cursor-pointer items-center overflow-hidden rounded-full bg-[#045f64] p-1 text-[#c6f56f] shadow-[0_1px_1px_rgba(16,24,40,0.16)] transition-[scale] duration-[280ms] ease-[cubic-bezier(0.32,0.72,0,1)] outline-none select-none focus-visible:ring-2 focus-visible:ring-[#c6f56f] not-disabled:active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100',\n        className\n      )}\n    >\n      <span className=\"absolute inset-y-1 left-1 z-10 w-13 overflow-hidden rounded-full bg-[#c6f56f] text-[#045f64] shadow-[0_53px_15px_0_rgba(0,1,15,0),0_34px_14px_0_rgba(0,1,15,0.03),0_19px_11px_0_rgba(0,1,15,0.09),0_8px_8px_0_rgba(0,1,15,0.15),0_2px_5px_0_rgba(0,1,15,0.18)] transition-[width,left] duration-[500ms] ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[icon-state=expand]:w-[calc(100%-8px)] group-data-[icon-state=expand]:duration-[640ms] group-data-[icon-state=park]:left-[calc(100%-3.25rem-0.25rem)] motion-reduce:transition-none\">\n        <ButtonIconSlideTrail iconClassName=\"fill-[#045f64]\" />\n      </span>\n      <span\n        className={cn(\n          'relative z-20 mr-3 ml-15 origin-center scale-100 font-[family-name:Outfit,sans-serif] text-base font-normal whitespace-nowrap transition-[scale,opacity,margin] duration-[500ms] ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[icon-state=expand]:scale-90 group-data-[icon-state=expand]:opacity-0 group-data-[icon-state=expand]:duration-[640ms] group-data-[icon-state=park]:mr-15 group-data-[icon-state=park]:ml-3 motion-reduce:!scale-100 motion-reduce:transition-none',\n          labelClassName\n        )}\n      >\n        {children}\n      </span>\n    </Tag>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/icon-trail-button"
}