{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "key",
  "title": "Key",
  "description": "A button made as a keycap: a cap with a lit top edge on a visible side, which travels down by the side's depth when pressed by pointer, Space or Enter. While its action runs it latches down with a small lamp lit, instead of a spinner, and pops back up when done.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json",
    "https://bevel-ui.vercel.app/r/feedback.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/key.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { cn } from \"cn\"\nimport { useBevelFeedback } from \"@/lib/feedback\"\n\ntype KeyContextValue = { pending: boolean }\n\nconst KeyContext = React.createContext<KeyContextValue>({ pending: false })\n\ntype KeyProps = ButtonPrimitive.Props & {\n  /** primary: the lit key for the main action. secondary: a plain cap. ghost: engraved, no body until pressed. danger: a red key. */\n  variant?: \"primary\" | \"secondary\" | \"ghost\" | \"danger\"\n  /** Heights match shadcn's buttons: sm 32px, md 36px, lg 40px, side included. */\n  size?: \"sm\" | \"md\" | \"lg\"\n  /** The key latches down while its action runs and pops back up when this goes false. */\n  pending?: boolean\n  /** A square cap for a single icon. Prefer IconKey, which also requires a label. */\n  square?: boolean\n}\n\n/**\n * A keycap button. The cap sits on a visible side and travels down by its\n * depth when pressed, by pointer, Space or Enter. While `pending`, it latches\n * down and stays there until the work is done.\n */\nfunction Key({\n  className,\n  variant = \"primary\",\n  size = \"md\",\n  pending = false,\n  square = false,\n  disabled = false,\n  focusableWhenDisabled,\n  onPointerDown,\n  onPointerUp,\n  onPointerLeave,\n  onPointerCancel,\n  onKeyDown,\n  onKeyUp,\n  onBlur,\n  children,\n  ...props\n}: KeyProps) {\n  const [down, setDown] = React.useState(false)\n  const feedback = useBevelFeedback()\n  const live = !disabled && !pending\n  const press = () => {\n    if (!live) return\n    setDown(true)\n    feedback.click()\n  }\n  const release = () => setDown(false)\n\n  React.useEffect(() => {\n    if (!live) setDown(false)\n  }, [live])\n\n  const context = React.useMemo(() => ({ pending }), [pending])\n  return (\n    <KeyContext value={context}>\n      <ButtonPrimitive\n        {...props}\n        onPointerDown={(event) => {\n          onPointerDown?.(event)\n          if (event.button === 0) press()\n        }}\n        onPointerUp={(event) => {\n          onPointerUp?.(event)\n          release()\n        }}\n        onPointerLeave={(event) => {\n          onPointerLeave?.(event)\n          release()\n        }}\n        onPointerCancel={(event) => {\n          onPointerCancel?.(event)\n          release()\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event)\n          if ((event.key === \" \" || event.key === \"Enter\") && !event.repeat) press()\n        }}\n        onKeyUp={(event) => {\n          onKeyUp?.(event)\n          if (event.key === \" \" || event.key === \"Enter\") release()\n        }}\n        onBlur={(event) => {\n          onBlur?.(event)\n          release()\n        }}\n        // A busy key keeps focus so keyboard users don't lose their place.\n        disabled={disabled || pending}\n        focusableWhenDisabled={focusableWhenDisabled ?? pending}\n        aria-busy={pending || undefined}\n        data-slot=\"key\"\n        data-variant={variant}\n        data-size={size}\n        data-square={square ? \"\" : undefined}\n        data-pressed={down ? \"\" : undefined}\n        data-state={pending ? \"pending\" : disabled ? \"disabled\" : undefined}\n        className={cn(\n          \"group/key relative isolate inline-flex shrink-0 flex-col rounded-(--bevel-radius-sm) pb-(--key-depth) align-middle outline-none select-none\",\n          \"h-9 data-[size=lg]:h-10 data-[size=sm]:h-8\",\n          \"data-square:w-9 data-square:data-[size=lg]:w-10 data-square:data-[size=sm]:w-8\",\n          \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\",\n          \"data-[state=disabled]:cursor-not-allowed data-[state=pending]:cursor-progress\",\n          \"forced-colors:border forced-colors:border-[ButtonText]\",\n          className as string\n        )}\n      >\n        {/* The side of the key: visible below the cap until it travels down over it. */}\n        <span\n          aria-hidden\n          data-slot=\"key-side\"\n          className=\"absolute inset-x-0 top-(--key-depth) bottom-0 -z-1 rounded-(--bevel-radius-sm) bg-(--key-cap-side) shadow-(--key-cast)\"\n        />\n        <span\n          data-slot=\"key-cap\"\n          className={cn(\n            \"flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-(--bevel-radius-sm) px-3 text-sm font-semibold whitespace-nowrap text-(--key-cap-foreground)\",\n            \"group-data-[size=lg]/key:px-4 group-data-[size=sm]/key:gap-1 group-data-[size=sm]/key:px-2.5 group-data-[size=sm]/key:text-xs group-data-square/key:px-0\",\n            \"bg-(--key-cap) bg-linear-to-b from-(--key-cap-sheen) to-transparent to-60% shadow-(--key-cap-light)\",\n            // Released: the spring pushes the cap back up with a small overshoot.\n            \"transition-[translate,filter] duration-(--bevel-duration-move) ease-(--bevel-ease-spring) motion-reduce:transition-none\",\n            \"group-hover/key:brightness-105\",\n            // Pressed: down by the side's depth, quickly.\n            \"group-data-pressed/key:translate-y-(--key-depth) group-data-pressed/key:duration-(--bevel-duration-press) group-data-pressed/key:ease-(--bevel-ease-out)\",\n            // Busy: latched down and dimmed.\n            \"group-data-[state=pending]/key:translate-y-(--key-depth) group-data-[state=pending]/key:brightness-90 group-data-[state=pending]/key:duration-(--bevel-duration-press) group-data-[state=pending]/key:ease-(--bevel-ease-out)\",\n            // Disabled: sunk into its housing, matte.\n            \"group-data-[state=disabled]/key:translate-y-(--key-depth) group-data-[state=disabled]/key:bg-none group-data-[state=disabled]/key:text-(--key-cap-foreground)/60 group-data-[state=disabled]/key:saturate-50 group-data-[state=disabled]/key:brightness-100\",\n            \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 group-data-[size=sm]/key:[&_svg:not([class*='size-'])]:size-3.5\"\n          )}\n        >\n          {children}\n        </span>\n      </ButtonPrimitive>\n    </KeyContext>\n  )\n}\n\n/**\n * A small lamp on the cap. Dark at rest; it glows while the key is pending.\n * Put it first or last among the key's children.\n */\nfunction KeyLamp({ className, ...props }: React.ComponentProps<\"span\">) {\n  const { pending } = React.useContext(KeyContext)\n  return (\n    <span\n      aria-hidden\n      data-slot=\"key-lamp\"\n      data-lit={pending ? \"\" : undefined}\n      className={cn(\"relative size-1.5 shrink-0 rounded-full bg-(--key-lamp-off) shadow-(--bevel-depth-pressed)\", className)}\n      {...props}\n    >\n      <span\n        className={cn(\n          \"bevel-led absolute inset-0 rounded-full opacity-0 transition-opacity duration-(--bevel-duration-small) ease-(--bevel-ease-out) motion-reduce:transition-none\",\n          \"in-data-lit:opacity-100\"\n        )}\n      />\n    </span>\n  )\n}\n\nexport { Key, KeyLamp }\nexport type { KeyProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--key-primary": "oklch(0.76 0.14 68)",
      "--key-primary-side": "oklch(0.58 0.13 55)",
      "--key-primary-foreground": "oklch(0.24 0.04 55)",
      "--key-secondary": "oklch(0.97 0.004 250)",
      "--key-secondary-side": "oklch(0.8 0.008 250)",
      "--key-secondary-foreground": "oklch(0.27 0.012 250)",
      "--key-danger": "oklch(0.55 0.19 27)",
      "--key-danger-side": "oklch(0.4 0.15 27)",
      "--key-danger-foreground": "oklch(0.98 0.01 27)",
      "--key-ghost-foreground": "currentColor",
      "--key-ghost-hover": "oklch(0.5 0.01 250 / 0.08)",
      "--key-ghost-well": "oklch(0.5 0.01 250 / 0.14)",
      "--key-lamp": "oklch(0.82 0.17 145)",
      "--key-lamp-off": "oklch(0.45 0.01 250 / 0.55)",
      "--key-travel-sm": "2px",
      "--key-travel": "3px",
      "--key-travel-lg": "3px"
    },
    ".dark": {
      "--key-primary": "oklch(0.74 0.13 68)",
      "--key-primary-side": "oklch(0.5 0.11 55)",
      "--key-secondary": "oklch(0.36 0.008 250)",
      "--key-secondary-side": "oklch(0.22 0.008 250)",
      "--key-secondary-foreground": "oklch(0.94 0.01 90)",
      "--key-danger": "oklch(0.56 0.18 27)",
      "--key-danger-side": "oklch(0.36 0.13 27)",
      "--key-ghost-hover": "oklch(1 0 0 / 0.06)",
      "--key-ghost-well": "oklch(0 0 0 / 0.3)"
    },
    "[data-slot=\"key\"]": {
      "--key-depth": "var(--key-travel)",
      "--key-cap": "var(--key-primary)",
      "--key-cap-side": "var(--key-primary-side)",
      "--key-cap-foreground": "var(--key-primary-foreground)",
      "--bevel-led": "var(--key-lamp)",
      "--key-cap-sheen": "var(--bevel-highlight)",
      "--key-cap-light": "inset 0 1px 0 var(--bevel-highlight-strong), inset 0 0 0 1px oklch(0 0 0 / 0.06)",
      "--key-cast": "0 1px 2px var(--bevel-cast)"
    },
    "[data-slot=\"key\"][data-size=\"sm\"]": {
      "--key-depth": "var(--key-travel-sm)"
    },
    "[data-slot=\"key\"][data-size=\"lg\"]": {
      "--key-depth": "var(--key-travel-lg)"
    },
    "[data-slot=\"key\"][data-variant=\"secondary\"]": {
      "--key-cap": "var(--key-secondary)",
      "--key-cap-side": "var(--key-secondary-side)",
      "--key-cap-foreground": "var(--key-secondary-foreground)"
    },
    "[data-slot=\"key\"][data-variant=\"danger\"]": {
      "--key-cap": "var(--key-danger)",
      "--key-cap-side": "var(--key-danger-side)",
      "--key-cap-foreground": "var(--key-danger-foreground)"
    },
    "[data-slot=\"key\"][data-variant=\"ghost\"]": {
      "--key-depth": "0px",
      "--key-cap": "transparent",
      "--key-cap-side": "transparent",
      "--key-cap-foreground": "var(--key-ghost-foreground)",
      "--key-cap-sheen": "transparent",
      "--key-cap-light": "none",
      "--key-cast": "none"
    },
    "[data-slot=\"key\"][data-variant=\"ghost\"]:hover:not([data-state])": {
      "--key-cap": "var(--key-ghost-hover)"
    },
    "[data-slot=\"key\"][data-variant=\"ghost\"]:is([data-pressed], [data-state=\"pending\"])": {
      "--key-cap": "var(--key-ghost-well)",
      "--key-cap-light": "var(--bevel-depth-pressed)"
    },
    "[data-slot=\"key\"][data-state=\"disabled\"]": {
      "--key-cap-light": "none"
    }
  },
  "categories": [
    "essentials"
  ],
  "type": "registry:ui"
}