{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "color-picker",
  "title": "Color Picker",
  "type": "registry:ui",
  "description": "Hex / RGB color input with a popover swatch grid. Supports controlled and uncontrolled modes, alpha channel, and a recent-colors row.",
  "categories": [
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/color-picker/color-picker.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { live } from 'lit/directives/live.js'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\nconst DEFAULT_PRESETS = [\n  '#ef4444',\n  '#f97316',\n  '#eab308',\n  '#22c55e',\n  '#14b8a6',\n  '#3b82f6',\n  '#8b5cf6',\n  '#ec4899',\n  '#ffffff',\n  '#d4d4d4',\n  '#737373',\n  '#171717',\n]\n\nconst stringListConverter = {\n  fromAttribute: (v: string | null): string[] => {\n    if (!v) return []\n    try {\n      const parsed = JSON.parse(v)\n      if (Array.isArray(parsed)) return parsed\n    } catch {\n      // split by comma\n    }\n    return v.split(',').map((s) => s.trim()).filter(Boolean)\n  },\n  toAttribute: (v: string[] | undefined): string | null => (v ? JSON.stringify(v) : null),\n}\n\n/**\n * <uip-color-picker> — the registry ColorPicker as a web component.\n *\n * Form-associated: submits `name=value` with its <form>, supports reset.\n */\nexport class UipColorPicker extends LitElement {\n  static formAssociated = true\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    value: { reflect: true },\n    defaultValue: { attribute: 'default-value' },\n    name: { reflect: true },\n    disabled: { type: Boolean, reflect: true },\n    hideHexInput: { type: Boolean, attribute: 'hide-hex-input', reflect: true },\n    presets: { converter: stringListConverter },\n    accessibleLabel: { attribute: 'aria-label' },\n  }\n\n  value = '#3b82f6'\n  defaultValue?: string\n  name?: string\n  disabled = false\n  hideHexInput = false\n  presets: string[] = DEFAULT_PRESETS\n  accessibleLabel?: string\n\n  private initialValue = ''\n  private internals = this.attachInternals()\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', 'color-picker')\n    if (this.defaultValue !== undefined && this.getAttribute('value') === null) {\n      this.value = this.defaultValue\n    }\n    this.initialValue = this.value\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('value')) {\n      this.internals.setFormValue(this.value ?? '')\n    }\n  }\n\n  formResetCallback() {\n    this.setValue(this.initialValue)\n  }\n\n  formDisabledCallback(disabled: boolean) {\n    this.disabled = disabled\n  }\n\n  private setValue(next: string) {\n    const changed = this.value !== next\n    this.value = next\n    if (changed) {\n      this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n      this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n      this.dispatchEvent(new CustomEvent('value-change', { detail: { value: next }, bubbles: true, composed: true }))\n    }\n  }\n\n  private onColorInput(e: Event) {\n    this.setValue((e.target as HTMLInputElement).value)\n  }\n\n  private onColorChange(e: Event) {\n    this.setValue((e.target as HTMLInputElement).value)\n  }\n\n  private onTextInput(e: Event) {\n    this.setValue((e.target as HTMLInputElement).value)\n  }\n\n  private onTextChange(e: Event) {\n    this.setValue((e.target as HTMLInputElement).value)\n  }\n\n  private selectPreset(color: string) {\n    if (this.disabled) return\n    this.setValue(color)\n  }\n\n  render() {\n    const swatches = this.presets ?? []\n    const safeColorValue = /^#[0-9a-fA-F]{6}$/.test(this.value || '') ? this.value : '#ffffff'\n\n    return html`\n      <div part=\"base\" class=\"space-y-3\">\n        <div class=\"flex items-center gap-2\">\n          <div\n            part=\"preview\"\n            class=\"border-input relative h-10 w-10 shrink-0 overflow-hidden rounded-md border shadow-xs\"\n            style=${styleMap({ backgroundColor: this.value || '#ffffff' })}\n          >\n            <input\n              type=\"color\"\n              .value=${live(safeColorValue)}\n              ?disabled=${this.disabled}\n              aria-label=${this.accessibleLabel ?? 'Pick color'}\n              class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed\"\n              @input=${this.onColorInput}\n              @change=${this.onColorChange}\n            />\n          </div>\n          ${!this.hideHexInput\n            ? html`<input\n                type=\"text\"\n                part=\"input\"\n                .value=${live(this.value || '')}\n                placeholder=\"#000000\"\n                spellcheck=\"false\"\n                autocomplete=\"off\"\n                ?disabled=${this.disabled}\n                aria-label=\"Hex color\"\n                class=\"bg-background border-input text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-10 flex-1 rounded-md border px-3 text-sm uppercase shadow-xs outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50\"\n                @input=${this.onTextInput}\n                @change=${this.onTextChange}\n              />`\n            : nothing}\n        </div>\n\n        ${swatches.length > 0\n          ? html`<div part=\"swatches\" class=\"flex flex-wrap gap-1.5\">\n              ${swatches.map(\n                (color) => html`\n                  <button\n                    type=\"button\"\n                    part=\"swatch\"\n                    ?disabled=${this.disabled}\n                    aria-label=${`Select ${color}`}\n                    style=${styleMap({ backgroundColor: color })}\n                    class=${cn(\n                      'ring-offset-background focus-visible:ring-ring/40 size-6 shrink-0 rounded-md shadow-sm transition-transform outline-none hover:scale-110 focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100',\n                      (this.value ?? '').toLowerCase() === color.toLowerCase()\n                        ? 'ring-foreground ring-2 ring-offset-2'\n                        : 'ring-border/50 ring-1',\n                      color.toLowerCase() === '#ffffff' && 'ring-border',\n                    )}\n                    @click=${() => this.selectPreset(color)}\n                  ></button>\n                `,\n              )}\n            </div>`\n          : nothing}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-color-picker') || customElements.define('uip-color-picker', UipColorPicker)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-color-picker': UipColorPicker\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/color-picker/color-picker.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
