{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "type": "registry:ui",
  "title": "Button",
  "description": "Roomy and slightly rounded, with the label at the title weight, in primary, secondary and ghost, on the shared control scale. Hover and press are instant and never move the box.",
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/button.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/button 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Button: the reference component. Read this before writing another one.\n *\n * - Slightly rounded, three sizes on the shared control scale (R9). Labels sit in\n *   .litt-trim, so side padding equals the visible air at sm and md. lg runs wider\n *   (--litt-button-pad-lg), so a short label never makes a square block.\n * - The label is at the title weight (R8, font-litt-title). State changes color and fill only; the\n *   box never moves or resizes (loading keeps the label mounted at opacity 0).\n * - Loading is busy, not disabled: the button keeps its variant's look and its focus (a disabled button drops focus\n *   to the page), and a click does nothing, a submit's included, until it ends. It does not sink while it waits.\n * - Material (R3), Press's body: a thick block on its own plate. The plate is the button's own border (--litt-slot, 3px at\n *   md and lg, 2px at sm); the block is the face inside it, with a lit top edge (--litt-highlight) and a darker wall along\n *   its foot that is its thickness. A secondary block casts a little (--litt-block-lift) and the primary is the accent with\n *   its glow. A press sinks the block into the plate: the wall goes, the cast tightens (--litt-block-press), the face\n *   takes its hover fill and the label drops, all at once, never faded (R2). Ghost has no plate and lies flat, and\n *   so does a disabled button: a plate with a flat face, nothing to push.\n * - Danger (R7): a destructive action is the same raised block with its words in --litt-danger. Inside a dialog's footer\n *   ([data-dialog-footer], matched structurally with Tailwind's in-* variant) it is the confirm of the dialog that\n *   asks, the one red fill: --litt-danger with --litt-danger-fg words and the primary's lit top edge, with no cast and no glow\n *   (the glow is the primary's, R6). Until the dialog's condition is met it is disabled, so it lies flat like any\n *   disabled button.\n * - Motion: the instant tier. Hover is a CSS transition on --litt-dur-instant, which tokens.css owns. A press drops the\n *   label twice --litt-press-depth (one pixel) rather than shrinking the box. Tailwind's translate-y-* sets the CSS\n *   `translate` property, not `transform`, so the transition names `translate`; naming `transform` instead would\n *   let the press snap.\n * - Touch: a pressed button also takes its hover fill, so a press still shows under reduced motion (which sets the\n *   depth to 0). The OS tap flash is turned off so the two do not double. Rows and segments fill (see Tabs, List).\n * - Children: one child is the label, trimmed to its cap height. An icon and a word\n *   (`<Button><PlusIcon /> New</Button>`, as shadcn writes it) sit side by side: each\n *   run of text is a trimmed label, and each element sits in the row. A child SVG is\n *   16px, like every button icon. The icon prop is still the documented way.\n * - Fun mode (motion/fun.tsx, a trial, off unless a <FunMode> is on): the press goes deeper (1px, so the label drops\n *   two) and the sink moves on fun mode's press spring, as a CSS linear() easing, so the release springs back with a little life. Only the\n *   sink's timing changes; fills keep the instant tier. Still CSS, no script per press. Reduced motion turns it off.\n * - Flat (proposals/flat-pill.md): the secondary is a grey pill with no cast (the tokens), icon-only is a grey circle,\n *   a ghost is words alone, and a disabled button is a grey fill with --litt-fg-disabled words and no hairline.\n * - Corners: rounded-litt-control (4px in litt.md v0.5). Icon-only buttons are\n *   round (pill) and need an aria-label.\n * - Bodies (proposals/worlds.md): this file is the contract (props, the real <button>, aria-busy and aria-disabled,\n *   the sizes, the label, the spinner) and Press's body. It resolves them once into ButtonBodyProps and picks a body\n *   by the material on <html> (byWorld, useMaterial). Flat's pill is bodies/button.flat.tsx; Device's key on its 2px\n *   skirt is bodies/button.device.tsx. A frame that sets its own material (a study strip, a side-by-side preview)\n *   wraps its buttons in a MaterialProvider to draw that body.\n */\nimport { Children, type ButtonHTMLAttributes, type ComponentPropsWithRef, type CSSProperties, type ReactNode } from \"react\";\nimport { byWorld, useFun, useMaterial } from \"../../lib/litt/motion\";\nimport { ButtonFlatBody } from \"./bodies/button.flat\";\nimport { ButtonDeviceBody } from \"./bodies/button.device\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n  /** Primary is the one accent fill in a view. Secondary is the default. Ghost is for quiet actions like Cancel. Danger is a destructive action, a block with red words; in a dialog's footer it is the confirm, filled red once it is enabled. */\n  variant?: ButtonVariant;\n  /** Height on the shared control scale: sm is 32, md is 44, lg is 56. Use lg for hero actions only, md in a page form and a dialog. */\n  size?: ButtonSize;\n  /** Swaps the label for a spinner and keeps the width. The button stays focusable and keeps its look, sets aria-busy and aria-disabled, and ignores clicks (a submit's too) until loading ends. */\n  loading?: boolean;\n  /** 16px icon before the label. */\n  icon?: ReactNode;\n  /** 16px icon after the label. */\n  iconRight?: ReactNode;\n  /** Round icon-only button. Requires aria-label. */\n  iconOnly?: boolean;\n  /** Fills the width of its container. */\n  block?: boolean;\n  ref?: React.Ref<HTMLButtonElement>;\n};\n\nconst base =\n  \"litt-target group relative inline-flex shrink-0 items-center justify-center gap-litt-control whitespace-nowrap select-none font-litt-title \" +\n  \"cursor-pointer transition-[background-color,color] duration-(--litt-dur-instant) ease-litt \" +\n  \"[-webkit-tap-highlight-color:transparent] border-(length:--plate) border-solid bg-clip-padding \" +\n  \"disabled:cursor-not-allowed data-loading:cursor-wait \" +\n  \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\nconst sizes: Record<ButtonSize, string> = {\n  sm: \"h-litt-control-sm px-litt-control-pad-sm text-litt-small\",\n  md: \"h-litt-control-md px-litt-control-pad-md text-litt-body\",\n  lg: \"h-litt-control-lg px-litt-button-pad-lg text-(length:--litt-control-text-lg)\",\n};\nconst iconSizes: Record<ButtonSize, string> = { sm: \"w-litt-control-sm\", md: \"w-litt-control-md\", lg: \"w-litt-control-lg\" };\n\n/* The plate is the border, so it takes room from the box. The side padding gives the same room back, and the button keeps the width it had. */\nconst plateSize: Record<ButtonSize, string> = {\n  sm: \"[--plate:2px] [padding-inline:calc(var(--litt-control-pad-sm)-2px)]\",\n  md: \"[--plate:3px] [padding-inline:calc(var(--litt-control-pad-md)-3px)]\",\n  lg: \"[--plate:3px] [padding-inline:calc(var(--litt-button-pad-lg)-3px)]\",\n};\nconst plateIcon = \"[--plate:3px] px-0\";\n\n/* The block's thickness is a 2px wall along its foot, drawn in the face's own background. A press sinks the block into its plate: the wall goes. */\nconst wall = \"[background-image:linear-gradient(to_top,var(--wall)_2px,transparent_2px)] enabled:not-data-loading:active:[background-image:none] disabled:[background-image:none]\";\n\nconst variants: Record<ButtonVariant, string> = {\n  primary:\n    \"border-(--litt-slot) bg-litt-accent text-litt-accent-fg hover:bg-litt-accent-hover enabled:pointer-coarse:active:bg-litt-accent-hover shadow-litt-glow \" +\n    \"inset-shadow-[0_1px_0_color-mix(in_oklab,white_22%,transparent)] [--wall:color-mix(in_oklab,black_24%,transparent)] \" + wall,\n  secondary:\n    \"border-(--litt-slot) bg-litt-block text-litt-fg [--stack:var(--litt-highlight),var(--litt-block-lift)] enabled:not-data-loading:active:[--stack:var(--litt-highlight),var(--litt-block-press)] \" +\n    \"shadow-(--stack) [--wall:color-mix(in_oklab,var(--litt-fg)_14%,transparent)] \" + wall + \" \" +\n    \"hover:bg-(--litt-block-hover) enabled:pointer-coarse:active:bg-(--litt-block-hover)\",\n  ghost:\n    \"bg-transparent bg-clip-border border-transparent text-litt-fg-muted hover:text-litt-fg hover:bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)] \" +\n    \"enabled:pointer-coarse:active:text-litt-fg enabled:pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-fg)_6%,transparent)]\",\n  danger:\n    \"border-(--litt-slot) bg-litt-block text-litt-danger [--stack:var(--litt-highlight),var(--litt-block-lift)] enabled:not-data-loading:active:[--stack:var(--litt-highlight),var(--litt-block-press)] \" +\n    \"shadow-(--stack) [--wall:color-mix(in_oklab,var(--litt-fg)_14%,transparent)] \" + wall + \" \" +\n    \"hover:bg-(--litt-block-hover) enabled:pointer-coarse:active:bg-(--litt-block-hover) \" +\n    /* A dialog's confirm. These rules sort after the block's, so they win its ties (active, a finger's press). */\n    \"in-[[data-dialog-footer]]:enabled:bg-litt-danger in-[[data-dialog-footer]]:enabled:text-litt-danger-fg \" +\n    \"in-[[data-dialog-footer]]:enabled:shadow-none in-[[data-dialog-footer]]:enabled:active:shadow-none \" +\n    \"in-[[data-dialog-footer]]:enabled:inset-shadow-[0_1px_0_color-mix(in_oklab,white_22%,transparent)] \" +\n    \"in-[[data-dialog-footer]]:enabled:[--wall:color-mix(in_oklab,black_24%,transparent)] \" +\n    \"in-[[data-dialog-footer]]:enabled:hover:bg-[color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_10%)] \" +\n    \"in-[[data-dialog-footer]]:enabled:pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_10%)]\",\n};\n/* Disabled lies flat: the plate stays, and its face has no cast, no lit edge and no wall. */\nconst disabledLook =\n  \"disabled:bg-litt-surface-2 disabled:bg-clip-padding disabled:border-(--litt-slot) disabled:text-litt-fg-disabled disabled:shadow-none disabled:inset-shadow-none \";\n\n/* One child is the label. Several (an icon and a word) sit in the row: text runs are trimmed labels, elements sit as they are. */\nfunction label(children: ReactNode): ReactNode {\n  const parts = Children.toArray(children);\n  if (parts.length < 2) return parts.length ? <span className=\"litt-trim\">{children}</span> : null;\n  const out: ReactNode[] = [];\n  let text = \"\";\n  const flush = () => {\n    const t = text.trim();\n    if (t) out.push(<span key={`label-${out.length}`} className=\"litt-trim\">{t}</span>);\n    text = \"\";\n  };\n  for (const part of parts) {\n    if (typeof part === \"string\" || typeof part === \"number\") text += part;\n    else { flush(); out.push(part); }\n  }\n  flush();\n  return out;\n}\n\n/** What a body gets from the contract: the resolved state, the geometry the control scale sets, and the element's attributes. A body adds its look and its data-body, and nothing else about what the button is. */\nexport type ButtonBodyProps = {\n  variant: ButtonVariant;\n  size: ButtonSize;\n  iconOnly: boolean;\n  loading: boolean;\n  disabled: boolean | undefined;\n  /** The control scale's height, side padding and text size for this size. */\n  sizeClass: string;\n  /** The corner: rounded-litt-control, or a round icon-only button's width and pill. */\n  shapeClass: string;\n  /** w-full when the button fills its container, else empty. */\n  blockClass: string;\n  /** The caller's className. */\n  className: string;\n  /** Fun mode's press (the deeper sink and its spring) as a style; undefined when fun mode is off. */\n  fun: CSSProperties | undefined;\n  /** The row: icon, label and trailing icon, mounted at opacity 0 while loading so the width holds. */\n  children: ReactNode;\n  /** The 16px spinner, for the body to place while loading. */\n  spinner: ReactNode;\n  /** type, disabled, aria-busy, aria-disabled, data-loading, the caller's style and other attributes, and the click handler that ignores presses while loading. */\n  rootProps: ComponentPropsWithRef<\"button\"> & { \"data-loading\"?: true };\n};\n\n/** Press's body: a thick block on its own plate, sinking into it when pressed. The default, and what a world without a body of its own gets. */\nfunction PressBody({ variant, size, iconOnly, sizeClass, shapeClass, blockClass, className, fun, children, spinner, loading, rootProps }: ButtonBodyProps) {\n  const { style, ...root } = rootProps;\n  return (\n    <button\n      {...root}\n      data-body=\"press\"\n      className={[base, sizeClass, iconOnly ? plateIcon : plateSize[size], variants[variant], disabledLook, shapeClass, blockClass, className].join(\" \")}\n      style={fun ? { ...fun, ...style } : style}\n    >\n      <span\n        className={\n          \"inline-flex items-center justify-center transition-[background-color,color,translate] duration-(--litt-dur-instant) ease-litt \" +\n          (variant === \"ghost\"\n            ? \"group-[:enabled:not([data-loading]):active]:translate-y-(--litt-press-depth)\"\n            : \"group-[:enabled:not([data-loading]):active]:translate-y-[calc(var(--litt-press-depth)*2)]\")\n        }\n        style={fun}\n      >\n        {children}\n      </span>\n      {loading && (\n        <span className=\"absolute inset-0 grid place-items-center\" aria-hidden=\"true\">\n          {spinner}\n        </span>\n      )}\n    </button>\n  );\n}\n\nexport function Button({\n  variant = \"secondary\",\n  size = \"md\",\n  loading = false,\n  icon,\n  iconRight,\n  iconOnly = false,\n  block = false,\n  className = \"\",\n  children,\n  disabled,\n  type = \"button\",\n  style,\n  onClick,\n  ...rest\n}: ButtonProps) {\n  const fun = useFun();\n  const material = useMaterial();\n  /* Fun mode: the sink (the third transitioned property) takes the press spring and goes deeper; the fills keep the instant tier. */\n  const press: CSSProperties | undefined = fun\n    ? ({\n        \"--litt-press-depth\": \"1px\",\n        transitionDuration: `var(--litt-dur-instant), var(--litt-dur-instant), ${fun.press.duration}ms`,\n        transitionTimingFunction: `var(--litt-ease-out), var(--litt-ease-out), ${fun.press.easing}`,\n      } as CSSProperties)\n    : undefined;\n  const Body = byWorld(material, { press: PressBody, flat: ButtonFlatBody, device: ButtonDeviceBody });\n  return (\n    <Body\n      variant={variant}\n      size={size}\n      iconOnly={iconOnly}\n      loading={loading}\n      disabled={disabled}\n      sizeClass={sizes[size]}\n      shapeClass={iconOnly ? `${iconSizes[size]} px-0 rounded-litt-pill` : \"rounded-litt-control\"}\n      blockClass={block ? \"w-full\" : \"\"}\n      className={className}\n      fun={press}\n      spinner={<Spinner />}\n      rootProps={{\n        type,\n        disabled,\n        \"aria-busy\": loading || undefined,\n        \"aria-disabled\": loading || undefined,\n        \"data-loading\": loading || undefined,\n        style,\n        ...rest,\n        onClick: loading ? (e) => e.preventDefault() : onClick,\n      }}\n    >\n      <span\n        className={\n          \"inline-flex items-center gap-litt-control transition-opacity duration-(--litt-dur-instant) ease-litt \" +\n          \"[&>svg]:size-4 [&>svg]:shrink-0 \" +\n          (loading ? \"opacity-0\" : \"opacity-100\")\n        }\n      >\n        {icon}\n        {!iconOnly && label(children)}\n        {iconOnly && children}\n        {iconRight}\n      </span>\n    </Body>\n  );\n}\n\n/** 16px spinner, 1.5px stroke, currentColor. The rotation is a loading signal, not decoration. */\nexport function Spinner({\n  size = 16,\n}: {\n  /** Width and height in px. */\n  size?: number;\n}) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 16 16\" fill=\"none\" className=\"animate-spin\" /* litt-allow: spinner */>\n      <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeOpacity=\".25\" strokeWidth=\"1.5\" />\n      <path d=\"M14.25 8A6.25 6.25 0 0 0 8 1.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n",
      "target": "components/litt/button.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/button.device.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/button 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Button, Device's body (proposals/worlds.md, Bodies; proposals/device.md): a key.\n *\n * - Anatomy: the button is the 44px target. Behind it sits the skirt, its own element, the key's face darkened and\n *   stepped 2px down; on it stands the face, on the Device key tokens (--litt-block, --litt-highlight, the dark rim). The label\n *   is in the mono at the title weight and rides on the face.\n * - Press: the face and its label sink the whole --litt-press-depth (2px) onto the skirt on the instant tier, and the lit\n *   edge gives way to the shade of a key that is down. Under reduced motion the depth is 0, so the key does not\n *   travel, and the fill still darkens to show the press. Fun mode's deeper sink is not used: a key's travel is its\n *   2px.\n * - Variants: primary is the key in the accent (Device's orange) with --litt-accent-fg words. Secondary is a key in the\n *   body's grey (--litt-block). Danger is that key with its words in --litt-danger; in a dialog's footer it is the confirm, the\n *   one red key. Ghost is a flat label with no key; pressed, it sinks into a well (--litt-tray with --litt-tray-shade), the\n *   label a half step down.\n * - A disabled button lies flat: no skirt, a hairline, --litt-fg-disabled words. Loading keeps the label mounted at\n *   opacity 0 and puts the spinner behind a small well of LCD glass (--litt-field, --litt-field-ink), so the width holds.\n * - The contract (props, aria-busy, aria-disabled, sizes, label, spinner) is button.tsx's.\n */\nimport type { ButtonBodyProps, ButtonVariant } from \"../button\";\n\nconst base =\n  \"litt-target group relative inline-flex shrink-0 items-center justify-center whitespace-nowrap select-none font-litt-title font-litt-mono \" +\n  \"cursor-pointer [-webkit-tap-highlight-color:transparent] disabled:cursor-not-allowed data-loading:cursor-wait \" +\n  \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\n/* The face's edge: the lit top, a darker lower edge and the rim. Down, the lit edge gives way to the shade of a key that is pressed. */\nconst edgeUp =\n  \"[--key-edge:var(--litt-highlight),inset_0_-1px_0_color-mix(in_oklab,black_10%,transparent),0_0_0_1px_color-mix(in_oklab,black_18%,transparent)]\";\nconst edgeDown =\n  \"enabled:not-data-loading:active:[--key-edge:inset_0_1px_2px_color-mix(in_oklab,black_14%,transparent),0_0_0_1px_color-mix(in_oklab,black_18%,transparent)]\";\nconst edgeDisabled = \"disabled:[--key-edge:inset_0_0_0_1px_var(--litt-line)]\";\n\nconst keyed =\n  edgeUp + \" \" + edgeDown + \" \" + edgeDisabled + \" disabled:[--key:var(--litt-surface-2)] disabled:text-litt-fg-disabled\";\n\nconst variants: Record<ButtonVariant, string> = {\n  primary:\n    keyed + \" text-litt-accent-fg [--key:var(--litt-accent)] hover:[--key:var(--litt-accent-hover)] \" +\n    \"enabled:active:[--key:color-mix(in_oklab,var(--litt-accent-hover),black_12%)]\",\n  secondary:\n    keyed + \" text-litt-fg [--key:var(--litt-block)] hover:[--key:var(--litt-block-hover)] \" +\n    \"enabled:active:[--key:color-mix(in_oklab,var(--litt-block),var(--litt-fg)_10%)]\",\n  danger:\n    /* The red words lean a third toward --litt-fg and the key goes darker under the hand, so the words keep 4.5:1 on the key in both themes. */\n    keyed + \" text-[color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_35%)] [--key:var(--litt-block)] hover:[--key:color-mix(in_oklab,var(--litt-block),black_6%)] \" +\n    \"enabled:active:[--key:color-mix(in_oklab,var(--litt-block),black_14%)] \" +\n    /* A dialog's confirm: the one red key. These rules sort after the key's, so they win its ties (hover, press). */\n    \"in-[[data-dialog-footer]]:enabled:text-litt-danger-fg in-[[data-dialog-footer]]:enabled:[--key:var(--litt-danger)] \" +\n    \"in-[[data-dialog-footer]]:enabled:hover:[--key:color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_10%)] \" +\n    \"in-[[data-dialog-footer]]:enabled:active:[--key:color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_18%)]\",\n  ghost:\n    \"text-litt-fg-muted [--key:transparent] [--key-edge:0_0_0_0_transparent] disabled:text-litt-fg-disabled disabled:[--key:var(--litt-surface-2)] \" +\n    \"disabled:[--key-edge:inset_0_0_0_1px_var(--litt-line)] \" +\n    \"hover:text-litt-fg hover:[--key:color-mix(in_oklab,var(--litt-fg)_6%,transparent)] \" +\n    \"enabled:not-data-loading:active:text-litt-fg enabled:not-data-loading:active:[--key:var(--litt-tray)] enabled:not-data-loading:active:[--key-edge:var(--litt-tray-shade)]\",\n};\n\nconst down = \"group-[:enabled:not([data-loading]):active]:translate-y-(--litt-press-depth)\";\nconst downHalf = \"group-[:enabled:not([data-loading]):active]:translate-y-[calc(var(--litt-press-depth)/2)]\";\n\nexport function ButtonDeviceBody({ variant, iconOnly, sizeClass, shapeClass, blockClass, className, children, spinner, loading, rootProps }: ButtonBodyProps) {\n  const { style, ...root } = rootProps;\n  const ghost = variant === \"ghost\";\n  const round = iconOnly ? \"rounded-litt-pill\" : \"rounded-litt-control\";\n  return (\n    <button\n      {...root}\n      data-body=\"device\"\n      className={[base, sizeClass, variants[variant], shapeClass, blockClass, className].join(\" \")}\n      style={style}\n    >\n      {!ghost && (\n        <span\n          aria-hidden=\"true\"\n          className={\n            \"pointer-events-none absolute inset-0 translate-y-[2px] bg-[color-mix(in_oklab,black_42%,var(--key))] \" + round +\n            \" shadow-[0_0_0_1px_color-mix(in_oklab,black_22%,transparent)] \" +\n            \"group-disabled:hidden forced-colors:hidden\"\n          }\n        />\n      )}\n      <span\n        className={\n          \"pointer-events-none absolute inset-0 bg-(--key) shadow-(--key-edge) \" + round + \" \" +\n          \"transition-[background-color,translate] duration-(--litt-dur-instant) ease-litt \" +\n          (ghost ? \"\" : down + \" \") +\n          \"forced-colors:border\"\n        }\n      />\n      <span\n        className={\n          \"relative inline-flex items-center justify-center gap-litt-control transition-[translate] duration-(--litt-dur-instant) ease-litt \" +\n          (ghost ? downHalf : down)\n        }\n      >\n        {children}\n        {loading && (\n          <span className=\"absolute inset-0 grid place-items-center\" aria-hidden=\"true\">\n            <span className=\"grid size-6 place-items-center rounded-litt-pill bg-litt-field text-litt-field-ink shadow-(--litt-field-shade) forced-colors:border\">\n              {spinner}\n            </span>\n          </span>\n        )}\n      </span>\n    </button>\n  );\n}\n",
      "target": "components/litt/bodies/button.device.tsx"
    },
    {
      "path": "registry/litt/ui/bodies/button.flat.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/button 2026.11.09. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Button, Flat's body (proposals/worlds.md, Bodies; proposals/flat-pill.md): a pill drawn by fill alone. Nothing casts,\n * catches the light or keeps a hairline, so the press shows in the fill: a step darker under the hand (hover, and the\n * finger's press on a touch screen).\n *\n * - Every button takes the control radius, which Flat opens at the pill (the one shape for what a hand touches), and\n *   follows a shape the Customize panel asks for. Primary is an ink pill (the accent fill where an accent is set), a step softer under the hand, with no\n *   glow and no highlight (Flat has no light, R2). Secondary is a grey pill (--litt-block is --litt-surface-3 under Flat),\n *   icon-only a grey circle. Ghost is words alone: no fill, even under the hand, which only brings the words to --litt-fg.\n *   Danger is the grey pill with red words; in a dialog's footer it is the confirm, the one red fill.\n * - A disabled button is a grey fill with --litt-fg-disabled words and no hairline.\n * - The contract (props, aria-busy, aria-disabled, sizes, label, spinner) is button.tsx's. This file only says what the\n *   pill is made of. Fun mode's deeper sink still applies, since it is a movement and not a look.\n */\nimport type { ButtonBodyProps, ButtonVariant } from \"../button\";\n\nconst base =\n  \"litt-target relative inline-flex shrink-0 items-center justify-center gap-litt-control whitespace-nowrap select-none font-litt-title \" +\n  \"cursor-pointer transition-[background-color,color,translate] duration-(--litt-dur-instant) ease-litt \" +\n  \"[-webkit-tap-highlight-color:transparent] \" +\n  \"active:translate-y-(--litt-press-depth) disabled:cursor-not-allowed disabled:active:translate-y-0 \" +\n  \"data-loading:cursor-wait data-loading:active:translate-y-0 \" +\n  \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus\";\n\nconst variants: Record<ButtonVariant, string> = {\n  primary: \"bg-litt-accent text-litt-accent-fg hover:bg-litt-accent-hover enabled:pointer-coarse:active:bg-litt-accent-hover\",\n  secondary: \"bg-litt-block text-litt-fg forced-colors:border hover:bg-(--litt-block-hover) enabled:pointer-coarse:active:bg-(--litt-block-hover)\",\n  ghost: \"bg-transparent text-litt-fg-muted hover:text-litt-fg enabled:pointer-coarse:active:text-litt-fg\",\n  danger:\n    \"bg-litt-block text-litt-danger forced-colors:border hover:bg-(--litt-block-hover) enabled:pointer-coarse:active:bg-(--litt-block-hover) \" +\n    /* A dialog's confirm. These rules sort after the pill's, so they win its ties (hover, a finger's press). */\n    \"in-[[data-dialog-footer]]:enabled:bg-litt-danger in-[[data-dialog-footer]]:enabled:text-litt-danger-fg \" +\n    \"in-[[data-dialog-footer]]:enabled:hover:bg-[color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_10%)] \" +\n    \"in-[[data-dialog-footer]]:enabled:pointer-coarse:active:bg-[color-mix(in_oklab,var(--litt-danger),var(--litt-fg)_10%)]\",\n};\n\nconst disabledLook = \"disabled:bg-litt-surface-3 disabled:text-litt-fg-disabled disabled:shadow-none disabled:inset-shadow-none\";\n\nexport function ButtonFlatBody({ variant, sizeClass, shapeClass, blockClass, className, fun, children, spinner, loading, rootProps }: ButtonBodyProps) {\n  const { style, ...root } = rootProps;\n  return (\n    <button\n      {...root}\n      data-body=\"flat\"\n      className={[base, sizeClass, variants[variant], disabledLook, shapeClass, blockClass, className].join(\" \")}\n      style={fun ? { ...fun, ...style } : style}\n    >\n      {children}\n      {loading && (\n        <span className=\"absolute inset-0 grid place-items-center\" aria-hidden=\"true\">\n          {spinner}\n        </span>\n      )}\n    </button>\n  );\n}\n",
      "target": "components/litt/bodies/button.flat.tsx"
    }
  ],
  "docs": "Import Button from components/litt/button. Primary is the one accent fill in a view. An icon-only button needs an aria-label.",
  "meta": {
    "version": "2026.11.09",
    "released": "2026-11-09",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
