{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "install-command",
  "type": "registry:ui",
  "title": "Install Command",
  "description": "A one-line command whose verb swaps per package manager while the package slides.",
  "categories": [
    "developer"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/copy-button.json"
  ],
  "files": [
    {
      "path": "components/ui/install-command.tsx",
      "content": "\"use client\";\nimport { Select } from \"@base-ui/react/select\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\nimport { cn } from \"@/lib/cn\";\nimport { Check, ChevronsUpDown } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\n\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\nexport type InstallAction = \"install\" | \"dev\" | \"global\" | \"remove\" | \"exec\" | \"create\" | \"run\";\n\nconst VERBS: Record<InstallAction, Record<PackageManager, string>> = {\n  install: { npm: \"npm install\", pnpm: \"pnpm add\", yarn: \"yarn add\", bun: \"bun add\" },\n  dev: { npm: \"npm install -D\", pnpm: \"pnpm add -D\", yarn: \"yarn add -D\", bun: \"bun add -d\" },\n  global: { npm: \"npm install -g\", pnpm: \"pnpm add -g\", yarn: \"yarn global add\", bun: \"bun add -g\" },\n  remove: { npm: \"npm uninstall\", pnpm: \"pnpm remove\", yarn: \"yarn remove\", bun: \"bun remove\" },\n  exec: { npm: \"npx\", pnpm: \"pnpm dlx\", yarn: \"yarn dlx\", bun: \"bunx\" },\n  create: { npm: \"npm create\", pnpm: \"pnpm create\", yarn: \"yarn create\", bun: \"bun create\" },\n  run: { npm: \"npm run\", pnpm: \"pnpm\", yarn: \"yarn\", bun: \"bun run\" },\n};\n\n/** The command a package manager uses for an action, e.g. (\"pnpm\", \"exec\") → \"pnpm dlx\". */\nexport const commandFor = (pm: PackageManager, action: InstallAction, args = \"\") => `${VERBS[action][pm]}${args ? ` ${args}` : \"\"}`;\n\n/* -------------------------------------------------------------------------------------------------\n * The synced preference. Same key and event as code tabs, so a docs page with both stays in step.\n * -----------------------------------------------------------------------------------------------*/\n\nconst EVENT = \"stealth:preference\";\nconst storageKey = (key: string) => `stealth:${key}`;\nconst read = (key: string) => {\n  try {\n    return localStorage.getItem(storageKey(key));\n  } catch {\n    return null;\n  }\n};\n\nfunction usePreference(key: string | undefined) {\n  const subscribe = useCallback(\n    (notify: () => void) => {\n      if (!key) return () => {};\n      const onStorage = (e: StorageEvent) => e.key === storageKey(key) && notify();\n      const onLocal = (e: Event) => (e as CustomEvent<{ key: string }>).detail?.key === key && notify();\n      window.addEventListener(\"storage\", onStorage);\n      window.addEventListener(EVENT, onLocal);\n      return () => {\n        window.removeEventListener(\"storage\", onStorage);\n        window.removeEventListener(EVENT, onLocal);\n      };\n    },\n    [key],\n  );\n  const value = useSyncExternalStore(subscribe, () => (key ? read(key) : null), () => null);\n  const set = useCallback(\n    (next: string) => {\n      if (!key) return;\n      try {\n        localStorage.setItem(storageKey(key), next);\n      } catch {}\n      window.dispatchEvent(new CustomEvent(EVENT, { detail: { key, value: next } }));\n    },\n    [key],\n  );\n  return [value, set] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * InstallCommand\n * -----------------------------------------------------------------------------------------------*/\n\nexport type InstallCommandProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  /** Everything after the verb: packages, flags, a script name. */\n  args?: string;\n  action?: InstallAction;\n  /** Which managers to offer, in order. One manager hides the switcher. */\n  managers?: PackageManager[];\n  value?: PackageManager;\n  defaultValue?: PackageManager;\n  onValueChange?: (pm: PackageManager) => void;\n  /** Shares the manager with every install command and code tabs using this key. `false` keeps it local. */\n  syncKey?: string | false;\n  /** Full commands per manager, for anything the actions don't cover. */\n  commands?: Partial<Record<PackageManager, string>>;\n};\n\nexport function InstallCommand({\n  args = \"\",\n  action = \"install\",\n  managers = [\"npm\", \"pnpm\", \"yarn\", \"bun\"],\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  syncKey = \"package-manager\",\n  commands,\n  className,\n  ...rest\n}: InstallCommandProps) {\n  const reduce = useReducedMotion();\n  const [synced, setSynced] = usePreference(syncKey || undefined);\n  const [inner, setInner] = useState<PackageManager>(defaultValue ?? managers[0]);\n  const offered = (v: string | null | undefined): v is PackageManager => !!v && managers.includes(v as PackageManager);\n  const pm: PackageManager = valueProp ?? (offered(synced) ? synced : offered(inner) ? inner : managers[0]);\n\n  const override = commands?.[pm];\n  const verb = override ? \"\" : VERBS[action][pm];\n  const tail = override ?? args;\n  const full = override ?? commandFor(pm, action, args);\n\n  const scroller = useRef<HTMLDivElement>(null);\n  useFade(scroller);\n\n  const choose = (next: PackageManager) => {\n    setInner(next);\n    setSynced(next);\n    onValueChange?.(next);\n  };\n\n  return (\n    <div\n      data-pm={pm}\n      className={cn(\n        \"flex h-10 min-w-0 items-center rounded-lg border border-line bg-raised pl-3 pr-1 shadow-[var(--shadow)]\",\n        className,\n      )}\n      {...rest}\n    >\n      <span aria-hidden className=\"mr-2 shrink-0 select-none font-mono text-[12.5px] text-fg-4\">\n        $\n      </span>\n\n      <div\n        ref={scroller}\n        className={cn(\n          \"min-w-0 flex-1 overflow-x-auto overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n          \"[mask-image:linear-gradient(to_left,transparent,var(--fg)_var(--fade-end,0px))]\",\n        )}\n      >\n        {/* One click selects the whole command, the way people want to grab it. */}\n        <code className=\"flex w-max select-all items-center whitespace-pre py-2 pr-3 font-mono text-[12.5px] leading-5 text-fg\">\n          <span className=\"flex\">\n            {verb && (\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  key={verb}\n                  className=\"text-fg-2\"\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: 7, filter: \"blur(3px)\" }}\n                  animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                  exit={reduce ? { opacity: 0, transition: { duration: 0.08 } } : { opacity: 0, y: -7, filter: \"blur(3px)\", transition: { duration: 0.14, ease: ease.in } }}\n                  transition={{ duration: reduce ? 0.12 : 0.24, ease: ease.out }}\n                >\n                  {verb}\n                </motion.span>\n              </AnimatePresence>\n            )}\n            {/* The package slides to where the new verb ends instead of jumping. */}\n            <motion.span layout={reduce ? false : \"position\"} transition={spring.snappy}>\n              {verb && tail ? \" \" : \"\"}\n              {tail}\n            </motion.span>\n          </span>\n        </code>\n      </div>\n\n      {managers.length > 1 && <ManagerSelect value={pm} managers={managers} action={action} onChange={choose} />}\n      <CopyButton value={full} iconOnly variant=\"ghost\" size=\"sm\" label=\"Copy command\" className=\"ml-0.5\" />\n    </div>\n  );\n}\n\nfunction ManagerSelect({\n  value,\n  managers,\n  action,\n  onChange,\n}: {\n  value: PackageManager;\n  managers: PackageManager[];\n  action: InstallAction;\n  onChange: (pm: PackageManager) => void;\n}) {\n  return (\n    <Select.Root value={value} onValueChange={(v) => v && onChange(v as PackageManager)} modal={false}>\n      <Select.Trigger\n        aria-label=\"Package manager\"\n        className={cn(\n          \"group/pm relative ml-1 inline-flex h-7 shrink-0 select-none items-center gap-1 rounded-md pl-2 pr-1.5 text-[12px] font-medium text-fg-2 outline-none\",\n          \"transition-[background-color,color,scale] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.96] active:duration-75\",\n          \"data-popup-open:bg-hover data-popup-open:text-fg\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"before:absolute before:-inset-y-2 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n        )}\n      >\n        {/* Every name shares one cell, so the trigger keeps one width whichever is picked. */}\n        <span className=\"grid text-left\">\n          {managers.map((m) => (\n            <span key={m} aria-hidden className=\"invisible col-start-1 row-start-1\">\n              {m}\n            </span>\n          ))}\n          <Select.Value className=\"col-start-1 row-start-1\" />\n        </span>\n        <ChevronsUpDown size={12} className=\"text-fg-4 transition-colors group-hover/pm:text-fg-3\" />\n      </Select.Trigger>\n      <Select.Portal>\n        <Select.Positioner alignItemWithTrigger={false} side=\"bottom\" align=\"end\" sideOffset={6} collisionPadding={8} className=\"z-(--z-popover) outline-none\">\n          <Select.Popup\n            className={cn(\n              \"min-w-40 origin-(--transform-origin) rounded-xl border border-line-2 bg-raised p-1 text-fg shadow-pop outline-none\",\n              \"transition-[opacity,scale,translate] duration-160 ease-out-expo\",\n              \"data-starting-style:-translate-y-0.5 data-starting-style:scale-[0.96] data-starting-style:opacity-0\",\n              \"data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-100 data-ending-style:ease-out\",\n              \"motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n            )}\n          >\n            <Select.List className=\"outline-none\">\n              {managers.map((m) => (\n                <Select.Item\n                  key={m}\n                  value={m}\n                  className={cn(\n                    \"flex h-8 cursor-default select-none items-center gap-2 rounded-lg pl-2 pr-2 text-[13px] outline-none pointer-coarse:h-10\",\n                    \"transition-colors duration-100 data-highlighted:bg-hover\",\n                  )}\n                >\n                  <Select.ItemText className=\"flex-1\">{m}</Select.ItemText>\n                  <span className=\"font-mono text-2xs text-fg-4\">{VERBS[action][m].split(\" \").slice(1).join(\" \") || VERBS[action][m]}</span>\n                  <span className=\"grid size-4 shrink-0 place-items-center\">\n                    <Select.ItemIndicator>\n                      <Check size={14} />\n                    </Select.ItemIndicator>\n                  </span>\n                </Select.Item>\n              ))}\n            </Select.List>\n          </Select.Popup>\n        </Select.Positioner>\n      </Select.Portal>\n    </Select.Root>\n  );\n}\n\n// Fades the right edge while the command runs past it, written straight to the element so scrolling never re-renders.\nfunction useFade(ref: React.RefObject<HTMLElement | null>) {\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    let frame = 0;\n    const update = () => {\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(() => {\n        const more = el.scrollWidth - el.clientWidth - el.scrollLeft > 1;\n        el.style.setProperty(\"--fade-end\", more ? \"24px\" : \"0px\");\n      });\n    };\n    update();\n    el.addEventListener(\"scroll\", update, { passive: true });\n    const ro = new ResizeObserver(update);\n    ro.observe(el);\n    if (el.firstElementChild) ro.observe(el.firstElementChild);\n    return () => {\n      cancelAnimationFrame(frame);\n      el.removeEventListener(\"scroll\", update);\n      ro.disconnect();\n    };\n  }, [ref]);\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/install-command"
}
