{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "package-command",
  "type": "registry:ui",
  "title": "Package Command",
  "description": "An install command in npm, pnpm, yarn and bun inside a small terminal card. A pill slides between the tabs, and only the characters that differ between managers flip like a split-flap board. Copying draws a check and sweeps a selection across the line. The choice is remembered and every block on the page follows it.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/package-command.tsx",
      "type": "registry:component",
      "target": "components/tweenui/package-command.tsx",
      "content": "'use client';\n\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type ComponentPropsWithoutRef,\n  type KeyboardEvent,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nexport const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'] as const;\n\nexport type PackageManager = (typeof PACKAGE_MANAGERS)[number];\n\nexport type PackageCommandKind = 'install' | 'execute';\n\nconst STORAGE_KEY = 'tweenui:package-manager';\nconst COPIED_FOR = 1600;\n\nconst listeners = new Set<() => void>();\nlet chosen: PackageManager | null = null;\nlet chosenByUser = false;\n\nconst toManager = (value: unknown) => PACKAGE_MANAGERS.find((manager) => manager === value);\n\nconst readManager = (): PackageManager => {\n  if (chosen) return chosen;\n  try {\n    chosen = toManager(window.localStorage.getItem(STORAGE_KEY)) ?? 'npm';\n  } catch {\n    chosen = 'npm';\n  }\n  return chosen;\n};\n\nconst chooseManager = (manager: PackageManager) => {\n  if (manager === chosen) return;\n  chosen = manager;\n  chosenByUser = true;\n  try {\n    window.localStorage.setItem(STORAGE_KEY, manager);\n  } catch {}\n  listeners.forEach((listener) => listener());\n};\n\nconst subscribe = (listener: () => void) => {\n  const onStorage = (event: StorageEvent) => {\n    if (event.key !== STORAGE_KEY) return;\n    const next = toManager(event.newValue);\n    if (!next || next === chosen) return;\n    chosen = next;\n    chosenByUser = true;\n    listeners.forEach((each) => each());\n  };\n  listeners.add(listener);\n  window.addEventListener('storage', onStorage);\n  return () => {\n    listeners.delete(listener);\n    window.removeEventListener('storage', onStorage);\n  };\n};\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst runner = (manager: PackageManager, kind: PackageCommandKind, dev: boolean) => {\n  if (kind === 'execute') {\n    if (manager === 'npm') return 'npx';\n    if (manager === 'bun') return 'bunx';\n    return `${manager} dlx`;\n  }\n  const verb = manager === 'npm' ? 'npm install' : `${manager} add`;\n  return dev ? `${verb} -D` : verb;\n};\n\nconst splitHead = (head: string) => {\n  const space = head.indexOf(' ');\n  return space === -1 ? [head, ''] : [head.slice(0, space), head.slice(space)];\n};\n\nconst FlipCells = ({ from, to }: { from: string; to: string }) => (\n  <span className=\"relative inline-flex items-center [clip-path:inset(0_-100vw)]\">\n    {Array.from(to, (character, index) => (\n      <span\n        key={index}\n        data-in\n        data-changed={character !== from[index] || undefined}\n        className=\"block w-[1ch]\"\n      >\n        {character}\n      </span>\n    ))}\n    {Array.from(from, (character, index) =>\n      character === to[index] || character === ' ' ? null : (\n        <span\n          key={`out-${index}`}\n          data-out\n          style={{ left: `${index}ch` }}\n          className=\"absolute inset-y-0 block w-[1ch]\"\n        >\n          {character}\n        </span>\n      )\n    )}\n  </span>\n);\n\nconst ClipboardIcon = () => (\n  <svg viewBox=\"0 0 24 24\" className=\"size-4\" fill=\"none\" aria-hidden=\"true\">\n    <rect x=\"8\" y=\"3\" width=\"8\" height=\"4\" rx=\"1\" stroke=\"currentColor\" strokeWidth=\"1.75\" />\n    <path\n      d=\"M16 5h1.5A1.5 1.5 0 0 1 19 6.5v12a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 18.5v-12A1.5 1.5 0 0 1 6.5 5H8\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.75\"\n      strokeLinecap=\"round\"\n    />\n  </svg>\n);\n\nexport interface PackageCommandProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onCopy'> {\n  packages: string | string[];\n  kind?: PackageCommandKind;\n  dev?: boolean;\n  managers?: readonly PackageManager[];\n  onCopy?: (command: string) => void;\n}\n\nconst PackageCommand = ({\n  packages,\n  kind = 'install',\n  dev = false,\n  managers = PACKAGE_MANAGERS,\n  onCopy,\n  className,\n  ...props\n}: PackageCommandProps) => {\n  const id = useId();\n  const panelId = `${id}-panel`;\n  const rootRef = useRef<HTMLDivElement>(null);\n  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const pillRef = useRef<HTMLSpanElement>(null);\n  const tailRef = useRef<HTMLSpanElement>(null);\n  const listRef = useRef<HTMLSpanElement>(null);\n  const sweepRef = useRef<HTMLSpanElement>(null);\n  const clipboardRef = useRef<HTMLSpanElement>(null);\n  const checkRef = useRef<SVGSVGElement>(null);\n  const checkPathRef = useRef<SVGPathElement>(null);\n  const copyTimer = useRef<number | undefined>(undefined);\n  const flipTimeline = useRef<gsap.core.Timeline | null>(null);\n\n  const stored = useSyncExternalStore(subscribe, readManager, () => 'npm');\n  const manager = managers.find((each) => each === stored) ?? managers[0] ?? 'npm';\n  const activeIndex = Math.max(0, managers.indexOf(manager));\n  const list = Array.isArray(packages) ? packages.join(' ') : packages.trim();\n  const head = runner(manager, kind, dev);\n  const command = `${head} ${list}`;\n\n  const [flip, setFlip] = useState({ from: head, to: head });\n  const [copied, setCopied] = useState(false);\n\n  if (flip.to !== head) setFlip({ from: flip.to, to: head });\n\n  const [fromTool, fromTail] = splitHead(flip.from);\n  const [toTool, toTail] = splitHead(flip.to);\n\n  const { contextSafe } = useGSAP({ scope: rootRef });\n\n  const highlight = (index: number) => {\n    tabRefs.current.forEach((tab, each) => {\n      if (tab) tab.dataset.highlighted = String(each === index);\n    });\n  };\n\n  const movePill = (index: number, animate: boolean) => {\n    const tab = tabRefs.current[index];\n    if (!tab) return;\n    highlight(index);\n    const bounds = { x: tab.offsetLeft, width: tab.offsetWidth };\n    if (!animate || prefersReducedMotion()) {\n      gsap.killTweensOf(pillRef.current);\n      gsap.set(pillRef.current, bounds);\n      return;\n    }\n    gsap.to(pillRef.current, { ...bounds, duration: 0.35, ease: 'power2.out', overwrite: 'auto' });\n  };\n\n  const followPointer = contextSafe((target: EventTarget) => {\n    if (!(target instanceof Element)) return;\n    const tab = target.closest('[role=\"tab\"]');\n    const index = tabRefs.current.findIndex((each) => each === tab);\n    if (index !== -1) movePill(index, true);\n  });\n\n  const returnToActive = contextSafe(() => movePill(activeIndex, true));\n\n  useGSAP(\n    () => {\n      movePill(activeIndex, chosenByUser);\n    },\n    { scope: rootRef, dependencies: [activeIndex, managers.length] }\n  );\n\n  useEffect(() => {\n    const tablist = tabRefs.current[0]?.parentElement;\n    if (!tablist || typeof ResizeObserver === 'undefined') return;\n    const observer = new ResizeObserver(() => movePill(activeIndex, false));\n    observer.observe(tablist);\n    return () => observer.disconnect();\n  }, [activeIndex, managers]);\n\n  useGSAP(\n    () => {\n      if (flip.from === flip.to) {\n        gsap.set(\n          [tailRef.current, listRef.current, ...gsap.utils.toArray('[data-in]', rootRef.current)],\n          {\n            clearProps: 'transform,opacity',\n          }\n        );\n        return;\n      }\n      flipTimeline.current?.kill();\n      const root = rootRef.current;\n      const listElement = listRef.current;\n      const incoming = gsap.utils.toArray<HTMLElement>('[data-in]', root);\n      gsap.set(incoming, { yPercent: 0, opacity: 1 });\n      const settle = () => setFlip((current) => ({ from: current.to, to: current.to }));\n      if (!chosenByUser || prefersReducedMotion()) {\n        gsap.set([tailRef.current, listElement], { x: 0 });\n        settle();\n        return;\n      }\n      const charWidth = incoming[0]?.getBoundingClientRect().width ?? 0;\n      const timeline = gsap.timeline({ onComplete: settle });\n      flipTimeline.current = timeline;\n      const slide = (element: HTMLElement | null, shift: number) => {\n        const currentX = Number(gsap.getProperty(element, 'x')) || 0;\n        timeline.fromTo(\n          element,\n          { x: shift * charWidth + currentX },\n          { x: 0, duration: 0.55, ease: 'power3.inOut' },\n          0\n        );\n      };\n      slide(tailRef.current, fromTool.length - toTool.length);\n      slide(listElement, flip.from.length - flip.to.length);\n      timeline.fromTo(\n        gsap.utils.toArray<HTMLElement>('[data-out]', root),\n        { yPercent: 0, opacity: 1 },\n        { yPercent: -70, opacity: 0, duration: 0.2, ease: 'power2.in' },\n        0\n      );\n      incoming\n        .filter((element) => element.hasAttribute('data-changed'))\n        .forEach((element, order) => {\n          timeline.fromTo(\n            element,\n            { yPercent: 100, opacity: 0 },\n            { yPercent: 0, opacity: 1, duration: 0.42, ease: 'power3.out' },\n            order * 0.03 + 0.1\n          );\n        });\n    },\n    { scope: rootRef, dependencies: [flip.from, flip.to] }\n  );\n\n  useEffect(() => () => window.clearTimeout(copyTimer.current), []);\n\n  const select = (index: number, focus = false) => {\n    const next = managers[(index + managers.length) % managers.length];\n    if (!next) return;\n    chooseManager(next);\n    if (focus) tabRefs.current[managers.indexOf(next)]?.focus();\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    const moves: Record<string, number> = {\n      ArrowRight: activeIndex + 1,\n      ArrowLeft: activeIndex - 1,\n      Home: 0,\n      End: managers.length - 1,\n    };\n    const target = moves[event.key];\n    if (target === undefined) return;\n    event.preventDefault();\n    select(target, true);\n  };\n\n  const playCopied = contextSafe(() => {\n    if (prefersReducedMotion()) {\n      gsap.set(clipboardRef.current, { opacity: 0 });\n      gsap.set(checkRef.current, { opacity: 1 });\n      gsap.set(checkPathRef.current, { strokeDashoffset: 0 });\n      return;\n    }\n    gsap\n      .timeline({ defaults: { overwrite: 'auto' } })\n      .to(clipboardRef.current, { scale: 0.6, opacity: 0, duration: 0.2, ease: 'power2.in' }, 0)\n      .set(checkRef.current, { opacity: 1 }, 0.12)\n      .fromTo(\n        checkPathRef.current,\n        { strokeDashoffset: 1 },\n        { strokeDashoffset: 0, duration: 0.4, ease: 'power3.out' },\n        0.12\n      )\n      .fromTo(\n        sweepRef.current,\n        { scaleX: 0, opacity: 1 },\n        { scaleX: 1, duration: 0.5, ease: 'power3.out' },\n        0\n      )\n      .to(sweepRef.current, { opacity: 0, duration: 0.5, ease: 'power2.out' }, 0.7);\n  });\n\n  const playReset = contextSafe(() => {\n    const reduced = prefersReducedMotion();\n    gsap.to(checkRef.current, { opacity: 0, duration: reduced ? 0 : 0.2, ease: 'power2.out' });\n    gsap.fromTo(\n      clipboardRef.current,\n      { scale: reduced ? 1 : 0.6 },\n      {\n        scale: 1,\n        opacity: 1,\n        duration: reduced ? 0 : 0.35,\n        ease: 'power3.out',\n        delay: reduced ? 0 : 0.1,\n      }\n    );\n  });\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(command);\n    } catch {\n      return;\n    }\n    onCopy?.(command);\n    setCopied(true);\n    playCopied();\n    window.clearTimeout(copyTimer.current);\n    copyTimer.current = window.setTimeout(() => {\n      setCopied(false);\n      playReset();\n    }, COPIED_FOR);\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      data-package-command\n      className={cn(\n        'w-full max-w-lg overflow-hidden rounded-xl border border-current/10 bg-current/[0.03] [--package-accent:#045f64] dark:[--package-accent:#c6f56f]',\n        className\n      )}\n      {...props}\n    >\n      <div className=\"flex items-center justify-between gap-3 border-b border-current/10 px-1.5 py-1.5\">\n        <div\n          role=\"tablist\"\n          aria-label=\"Package manager\"\n          className=\"relative flex\"\n          onPointerOver={(event) => followPointer(event.target)}\n          onPointerLeave={returnToActive}\n          onFocus={(event) => followPointer(event.target)}\n          onBlur={(event) => {\n            if (!event.currentTarget.contains(event.relatedTarget)) returnToActive();\n          }}\n        >\n          <span\n            ref={pillRef}\n            aria-hidden=\"true\"\n            className=\"absolute inset-y-0 left-0 rounded-md bg-current/[0.08]\"\n          />\n          {managers.map((each, index) => {\n            const selected = index === activeIndex;\n            return (\n              <button\n                key={each}\n                ref={(node) => {\n                  tabRefs.current[index] = node;\n                }}\n                id={`${id}-${each}`}\n                type=\"button\"\n                role=\"tab\"\n                aria-selected={selected}\n                aria-controls={panelId}\n                tabIndex={selected ? 0 : -1}\n                onClick={() => select(index)}\n                onKeyDown={handleKeyDown}\n                data-highlighted={selected}\n                className=\"relative cursor-pointer rounded-md px-3 py-1.5 font-mono text-xs opacity-55 transition-opacity duration-300 focus-visible:outline-2 focus-visible:outline-current data-[highlighted=true]:opacity-100 motion-reduce:transition-none\"\n              >\n                {each}\n              </button>\n            );\n          })}\n        </div>\n\n        <button\n          type=\"button\"\n          onClick={copy}\n          aria-label={copied ? 'Copied' : 'Copy command'}\n          className=\"relative grid size-8 shrink-0 cursor-pointer place-items-center rounded-md opacity-70 transition-[opacity,background-color] duration-200 hover:bg-current/[0.08] hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-current\"\n        >\n          <span ref={clipboardRef} className=\"flex\">\n            <ClipboardIcon />\n          </span>\n          <svg\n            ref={checkRef}\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            aria-hidden=\"true\"\n            className=\"absolute size-4 text-(--package-accent) opacity-0\"\n          >\n            <path\n              ref={checkPathRef}\n              d=\"M5 12.5l4.5 4.5L19 7.5\"\n              pathLength={1}\n              stroke=\"currentColor\"\n              strokeWidth=\"2.25\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeDasharray=\"1\"\n              strokeDashoffset=\"1\"\n            />\n          </svg>\n        </button>\n      </div>\n\n      <div\n        id={panelId}\n        role=\"tabpanel\"\n        aria-labelledby={`${id}-${manager}`}\n        className=\"overflow-x-auto px-4 py-3.5\"\n      >\n        <p className=\"relative inline-flex items-center font-mono text-sm leading-6 whitespace-pre\">\n          <span\n            ref={sweepRef}\n            aria-hidden=\"true\"\n            className=\"absolute -inset-x-1 -inset-y-0.5 origin-left scale-x-0 rounded bg-(--package-accent)/15\"\n          />\n          <span aria-hidden=\"true\" className=\"relative text-(--package-accent) select-none\">\n            ${' '}\n          </span>\n          <span aria-hidden=\"true\" className=\"relative inline-flex items-center\">\n            <FlipCells from={fromTool} to={toTool} />\n            <span ref={tailRef} className=\"inline-flex items-center\">\n              <FlipCells from={fromTail} to={toTail} />\n            </span>\n            <span ref={listRef} className=\"block opacity-70\">\n              {' '}\n              {list}\n            </span>\n          </span>\n          <span className=\"sr-only\">{command}</span>\n        </p>\n      </div>\n\n      <span role=\"status\" className=\"sr-only\">\n        {copied ? 'Copied to clipboard' : ''}\n      </span>\n    </div>\n  );\n};\n\nexport default PackageCommand;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/package-command"
}