{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "environment-variables",
  "title": "Environment Variables",
  "description": "A secret-aware configuration surface with masked values and explicit reveal or copy actions.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0"
  ],
  "registryDependencies": [
    "button",
    "https://pastaui.com/r/pastaui-styles.json"
  ],
  "files": [
    {
      "path": "src/components/ai/environment-variables.tsx",
      "content": "import { Menu } from '@base-ui/react/menu'\nimport { AddIcon as PlusIcon } from '@solar-icons/react/linear/add'\nimport { CopyIcon } from '@solar-icons/react/linear/copy'\nimport { EyeIcon } from '@solar-icons/react/linear/eye'\nimport { EyeClosedIcon as EyeOffIcon } from '@solar-icons/react/linear/eye-closed'\nimport { GlobalIcon } from '@solar-icons/react/linear/global'\nimport { KeyMinimalisticIcon as KeyRoundIcon } from '@solar-icons/react/linear/key-minimalistic'\nimport { MenuDotsIcon as MoreIcon } from '@solar-icons/react/linear/menu-dots'\nimport * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { cn } from 'cn'\n\ninterface EnvironmentVariable {\n  id: string\n  key: string\n  public?: boolean\n  scope?: string\n  value: string\n}\n\ninterface EnvironmentVariablesProps extends Omit<\n  React.ComponentProps<'section'>,\n  'onCopy' | 'onToggle'\n> {\n  items: EnvironmentVariable[]\n  onAdd?: () => void\n  onCopy?: (id: string) => void\n  onDelete?: (id: string) => void\n  onEdit?: (id: string) => void\n  onToggle?: (id: string) => void\n  revealedIds?: string[]\n  title?: string\n}\n\nconst REVEAL_DURATION = 10000\n\nconst swapClass =\n  'grid *:col-start-1 *:row-start-1 *:transition-[opacity,scale] *:duration-150 *:ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:*:transition-opacity'\n\nconst actionClass =\n  'rounded-full text-muted-foreground opacity-0 transition-[opacity,background-color] duration-150 ease-out group-focus-within:opacity-100 group-hover:opacity-100 hover:text-foreground pointer-coarse:opacity-100 data-popup-open:opacity-100'\n\nfunction maskValue(value: string) {\n  return `••••••••••••${value.slice(-4)}`\n}\n\nfunction EnvironmentVariables({\n  className,\n  items,\n  onAdd,\n  onCopy,\n  onDelete,\n  onEdit,\n  onToggle,\n  revealedIds,\n  title = 'Environment variables',\n  ...props\n}: EnvironmentVariablesProps) {\n  const [revealedId, setRevealedId] = React.useState<string | null>(null)\n  const [copiedId, setCopiedId] = React.useState<string | null>(null)\n  const revealedSet = new Set(revealedIds ?? (revealedId ? [revealedId] : []))\n\n  React.useEffect(() => {\n    if (!revealedId) return\n    const timer = window.setTimeout(() => setRevealedId(null), REVEAL_DURATION)\n    return () => window.clearTimeout(timer)\n  }, [revealedId])\n\n  React.useEffect(() => {\n    if (!copiedId) return\n    const timer = window.setTimeout(() => setCopiedId(null), 1500)\n    return () => window.clearTimeout(timer)\n  }, [copiedId])\n\n  function toggle(id: string) {\n    if (revealedIds) onToggle?.(id)\n    else setRevealedId((current) => (current === id ? null : id))\n  }\n\n  async function copy(item: EnvironmentVariable) {\n    await navigator.clipboard?.writeText(item.value)\n    setCopiedId(item.id)\n    onCopy?.(item.id)\n  }\n\n  return (\n    <section\n      data-slot=\"environment-variables\"\n      className={cn(\n        'overflow-hidden rounded-[16px] bg-card font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card',\n        className,\n      )}\n      {...props}\n    >\n      <header className=\"relative flex min-h-14 items-center gap-2 px-5 py-3 after:absolute after:right-5 after:bottom-0 after:left-5 after:border-b-[0.5px] after:border-foreground/[0.09] after:content-['']\">\n        <KeyRoundIcon className=\"size-4 text-muted-foreground\" />\n        <h3 className=\"text-sm leading-5 font-medium text-muted-foreground\">{title}</h3>\n        <span className=\"ml-auto text-[13px] leading-5 text-muted-foreground/70 tabular-nums\">\n          {items.length} configured\n        </span>\n        {onAdd && (\n          <Button\n            className=\"h-7 rounded-full bg-foreground/8 px-2.5 text-xs text-foreground/85 hover:bg-foreground/12 hover:text-foreground\"\n            onClick={onAdd}\n            size=\"xs\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <PlusIcon data-icon=\"inline-start\" /> Add\n          </Button>\n        )}\n      </header>\n      <ul>\n        {items.map((item) => {\n          const revealed = revealedSet.has(item.id)\n          const copied = copiedId === item.id\n\n          return (\n            <li\n              className=\"group relative flex min-h-16 items-center gap-3 px-5 py-3 after:absolute after:right-5 after:bottom-0 after:left-5 after:border-b-[0.5px] after:border-foreground/[0.09] after:content-[''] last:after:hidden\"\n              key={item.id}\n            >\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n                  <span className=\"min-w-0 truncate font-mono text-[13px] text-foreground\">\n                    {item.key}\n                  </span>\n                  {item.scope && (\n                    <span className=\"rounded-md bg-foreground/6 px-1.5 py-px text-xs leading-5 text-muted-foreground\">\n                      {item.scope}\n                    </span>\n                  )}\n                  {item.public && (\n                    <span className=\"flex items-center gap-1 text-xs leading-5 text-muted-foreground\">\n                      <GlobalIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                      Public\n                    </span>\n                  )}\n                </div>\n                <p\n                  className=\"mt-1 truncate font-mono text-[13px] text-muted-foreground transition-opacity duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] starting:opacity-0\"\n                  key={revealed || item.public ? 'value' : 'masked'}\n                >\n                  {item.public || revealed ? item.value : maskValue(item.value)}\n                </p>\n              </div>\n              <div className=\"flex shrink-0 items-center gap-0.5\">\n                {!item.public && (\n                  <Button\n                    aria-label={`${revealed ? 'Hide' : 'Reveal'} ${item.key}`}\n                    className={cn(actionClass, revealed && 'opacity-100')}\n                    onClick={() => toggle(item.id)}\n                    size=\"icon-sm\"\n                    type=\"button\"\n                    variant=\"ghost\"\n                  >\n                    <span className={swapClass}>\n                      <EyeIcon className={cn(revealed && 'scale-75 opacity-0')} />\n                      <EyeOffIcon className={cn(!revealed && 'scale-75 opacity-0')} />\n                    </span>\n                  </Button>\n                )}\n                <Button\n                  aria-label={copied ? `Copied ${item.key}` : `Copy ${item.key}`}\n                  className={cn(actionClass, copied && 'opacity-100')}\n                  onClick={() => void copy(item)}\n                  size=\"icon-sm\"\n                  type=\"button\"\n                  variant=\"ghost\"\n                >\n                  <span className={swapClass}>\n                    <CopyIcon className={cn(copied && 'scale-75 opacity-0')} />\n                    <svg\n                      aria-hidden=\"true\"\n                      className={cn(\n                        'size-4 text-emerald-600 dark:text-emerald-500',\n                        !copied && 'scale-75 opacity-0',\n                      )}\n                      fill=\"none\"\n                      stroke=\"currentColor\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth=\"1.75\"\n                      viewBox=\"0 0 16 16\"\n                    >\n                      <path d=\"m3.5 8.5 3 3 6-7\" />\n                    </svg>\n                  </span>\n                </Button>\n                {(onEdit || onDelete) && (\n                  <Menu.Root>\n                    <Menu.Trigger\n                      render={\n                        <Button\n                          aria-label={`More actions for ${item.key}`}\n                          className={actionClass}\n                          size=\"icon-sm\"\n                          type=\"button\"\n                          variant=\"ghost\"\n                        />\n                      }\n                    >\n                      <MoreIcon />\n                    </Menu.Trigger>\n                    <Menu.Portal>\n                      <Menu.Positioner align=\"end\" className=\"z-50\" side=\"bottom\" sideOffset={6}>\n                        <Menu.Popup className=\"min-w-32 origin-(--transform-origin) rounded-[14px] bg-card p-1 font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card transition-[opacity,scale] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] outline-none data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-opacity\">\n                          {onEdit && (\n                            <Menu.Item\n                              className=\"cursor-pointer rounded-[10px] px-3 py-2 outline-none data-highlighted:bg-foreground/6 data-highlighted:text-foreground\"\n                              onClick={() => onEdit(item.id)}\n                            >\n                              Edit\n                            </Menu.Item>\n                          )}\n                          {onDelete && (\n                            <Menu.Item\n                              className=\"cursor-pointer rounded-[10px] px-3 py-2 text-destructive outline-none data-highlighted:bg-destructive/10\"\n                              onClick={() => onDelete(item.id)}\n                            >\n                              Delete\n                            </Menu.Item>\n                          )}\n                        </Menu.Popup>\n                      </Menu.Positioner>\n                    </Menu.Portal>\n                  </Menu.Root>\n                )}\n              </div>\n            </li>\n          )\n        })}\n      </ul>\n    </section>\n  )\n}\n\nexport { EnvironmentVariables }\nexport type { EnvironmentVariable, EnvironmentVariablesProps }\n",
      "type": "registry:component",
      "target": "@components/ai/environment-variables.tsx"
    }
  ],
  "type": "registry:component"
}