{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "chip",
  "title": "Chip",
  "type": "registry:ui",
  "description": "Compact, removable tag — typically used inside `tags-input` or as a filter pill. Seven variants (`default`, `filled`, `outlined`, `elevated`, `success`, `warning`, `destructive`), three sizes, and an optional close button.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/chip/chip.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { X } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\nimport { chipVariants, type ChipVariants } from './chip.variants'\n\ntype Variant = NonNullable<ChipVariants['variant']>\ntype Size = NonNullable<ChipVariants['size']>\n\n/*\n * Enter/leave keyframes — React's injected `chip-motion-styles` verbatim.\n * Keyframes have no utility equivalent (same exception as skeleton's shimmer).\n */\nconst motion = css`\n  @keyframes chip-enter {\n    from {\n      opacity: 0;\n      transform: scale(0.88);\n    }\n    to {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n  @keyframes chip-leave {\n    to {\n      opacity: 0;\n      transform: scale(0.88);\n    }\n  }\n  [data-slot='chip'].chip-enter {\n    animation: chip-enter 180ms cubic-bezier(0.22, 1.2, 0.36, 1) both;\n  }\n  [data-slot='chip'].chip-leave {\n    animation: chip-leave 160ms ease-in both;\n    pointer-events: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot='chip'].chip-enter,\n    [data-slot='chip'].chip-leave {\n      animation: none !important;\n    }\n  }\n`\n\n/**\n * <uip-chip> — the registry Chip.\n *\n * Class strings are React's `chipVariants` verbatim, except `[&>span]:min-w-0`\n * becomes `[&::slotted(span)]:min-w-0` on the <slot> (a light-DOM <span> isn't\n * a shadow-tree child).\n *\n * Events: `close` (no detail) — React's `onClose`, fired after the 160ms\n * leave animation when the built-in dismiss button is clicked. Like React,\n * the chip does not remove itself; the listener does.\n */\nexport class UipChip extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, motion, css`:host { display: inline-flex; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    size: { reflect: true },\n    wrap: { type: Boolean, reflect: true },\n    closable: { type: Boolean, reflect: true },\n    leaving: { state: true },\n  }\n\n  variant: Variant = 'default'\n  size: Size = 'default'\n  wrap = false\n  closable = false\n  private leaving = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'chip')\n  }\n\n  willUpdate() {\n    if (this.leaving) this.setAttribute('data-leaving', 'true')\n    else this.removeAttribute('data-leaving')\n  }\n\n  private handleClose(e: MouseEvent) {\n    e.stopPropagation()\n    if (this.leaving) return\n    this.leaving = true\n    const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches\n    setTimeout(() => this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })), reduce ? 0 : 160)\n  }\n\n  render() {\n    return html`<span\n      part=\"base\"\n      data-slot=\"chip\"\n      data-leaving=${this.leaving ? 'true' : nothing}\n      class=${cn(\n        chipVariants({ variant: this.variant, size: this.size, wrap: this.wrap || undefined }),\n        'chip-enter',\n        this.leaving && 'chip-leave',\n      )}\n    >\n      <slot class=\"[&::slotted(span)]:min-w-0\"></slot>\n      ${this.closable\n        ? html`<button\n            type=\"button\"\n            aria-label=\"Remove item\"\n            class=\"focus-visible:ring-ring hover:bg-foreground/10 ml-1 inline-flex min-h-6 min-w-6 items-center justify-center rounded-full transition-transform duration-150 focus-visible:ring-1 focus-visible:outline-none active:scale-90\"\n            @click=${this.handleClose}\n          >\n            ${icon(X, 'x', 'size-3')}\n          </button>`\n        : nothing}\n    </span>`\n  }\n}\n\n/**\n * <uip-chip-group> — the registry ChipGroup (`role=\"group\"` flex wrapper).\n *\n * React's render-prop (`isSelected` / `toggle`) becomes the `isSelected(value)`\n * and `toggle(value)` methods with React's selection rules (multiple, max,\n * mandatory, disabled). `selected` is a string[] property. Unlike React's\n * controlled-only prop, `toggle` also updates `selected` before emitting.\n *\n * Events: `selected-change` (detail: { value: string[] }) — React's\n * `onSelectedChange`.\n */\nexport class UipChipGroup extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    selected: { type: Array },\n    multiple: { type: Boolean, reflect: true },\n    filter: { type: Boolean, reflect: true },\n    column: { type: Boolean, reflect: true },\n    mandatory: { type: Boolean },\n    max: { type: Number },\n    disabled: { type: Boolean, reflect: true },\n  }\n\n  selected: string[] = []\n  multiple = false\n  filter = false\n  column = false\n  mandatory = false\n  max?: number\n  disabled = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-chip-group', 'true')\n  }\n\n  isSelected(value: string) {\n    return this.selected.includes(value)\n  }\n\n  toggle(value: string) {\n    if (this.disabled) return\n    const selected = this.selected\n    let next: string[]\n    if (this.multiple) {\n      if (this.isSelected(value)) next = selected.filter((v) => v !== value)\n      else if (this.max && selected.length >= this.max) next = [...selected.slice(1), value]\n      else next = [...selected, value]\n    } else {\n      next = this.isSelected(value) && !this.mandatory ? [] : [value]\n    }\n    this.selected = next\n    this.dispatchEvent(new CustomEvent('selected-change', { detail: { value: next }, bubbles: true, composed: true }))\n  }\n\n  render() {\n    return html`<div\n      part=\"base\"\n      role=\"group\"\n      class=${cn('flex flex-wrap gap-2', this.column && 'flex-col')}\n      data-chip-group=\"true\"\n      data-multiple=${this.multiple ? 'true' : nothing}\n      data-filter=${this.filter ? 'true' : nothing}\n    >\n      <slot></slot>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-chip') || customElements.define('uip-chip', UipChip)\ncustomElements.get('uip-chip-group') || customElements.define('uip-chip-group', UipChipGroup)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-chip': UipChip\n    'uip-chip-group': UipChipGroup\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/chip/chip.ts"
    },
    {
      "path": "packages/elements-lit/src/components/chip/chip.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n */\n\nexport const chipVariants = cva(\n  // text-overflow/ellipsis cannot apply to a flex container's anonymous text,\n  // so truncation is opt-in via a child span (`<Chip><span class=\"truncate\">\n  // …</span></Chip>`); min-w-0 lets that span shrink inside the flex root.\n  'inline-flex items-center justify-center gap-1 rounded-full text-xs font-medium w-fit max-w-full whitespace-nowrap shrink-0 [&>span]:min-w-0 transition-colors duration-200 overflow-hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n  {\n    variants: {\n      variant: {\n        default: 'bg-muted text-muted-foreground hover:bg-muted/80',\n        filled: 'bg-primary text-primary-foreground hover:bg-primary/90',\n        outlined: 'border border-current bg-transparent hover:bg-accent',\n        outline: 'border border-current bg-transparent hover:bg-accent',\n        elevated: 'bg-primary/10 text-primary shadow-sm hover:bg-primary/20',\n        success: 'bg-success/10 text-success dark:text-success hover:bg-success/20',\n        warning: 'bg-warning/10 text-warning dark:text-warning hover:bg-warning/20',\n        destructive: 'bg-destructive/10 text-destructive dark:text-destructive hover:bg-destructive/20',\n      },\n      size: {\n        sm: 'h-6 px-2 text-xs',\n        default: 'h-7 px-2.5 text-xs',\n        lg: 'h-8 px-3 text-sm',\n      },\n      // Allow multi-line labels. rounded-lg + extra leading keeps a wrapped\n      // chip readable instead of a cramped two-line stadium pill.\n      wrap: {\n        true: 'whitespace-normal h-auto rounded-lg py-1 leading-snug',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n)\n\nexport type ChipVariants = VariantProps<typeof chipVariants>\n",
      "type": "registry:file",
      "target": "src/uip/components/chip/chip.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
