{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "heatmap",
  "title": "Heatmap",
  "description": "Heatmap from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/data-state-panel.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/skill-category.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/heatmap.tsx",
      "content": "'use client';\n\nimport { Grid3x3 } from 'lucide-react';\nimport { Children, forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode, type TdHTMLAttributes } from 'react';\n\nimport { focusRing, transitionBase } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { DataStatePanel, SkeletonBar } from '@/components/ui/data-state-panel';\nimport type { SkillCategory } from '@/components/ui/skill-category';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// Full class strings so Tailwind's content scan finds every token.\nconst heatmapTone = {\n  free: 'border-transparent bg-data-workload-free-bg text-data-workload-free-fg',\n  ok: 'border-transparent bg-data-workload-ok-bg text-data-workload-ok-fg',\n  over: 'border-transparent bg-data-workload-over-bg text-data-workload-over-fg',\n  need: 'border-dashed border-data-workload-need-border bg-data-workload-need-bg text-data-workload-need-fg',\n  empty: 'border-transparent bg-data-workload-empty-bg text-data-workload-empty-fg',\n  // Revenue states: paid is settled, unpaid is open, forecast is planned and not yet invoiced.\n  paid: 'border-transparent bg-general-success-container text-general-success-on-container',\n  unpaid: 'border-transparent bg-general-warning-container text-general-warning-on-container',\n  forecast: 'border-dashed border-border-default bg-transparent text-muted-foreground',\n  // Continuous scale: the fill is a brand overlay whose opacity follows `intensity`.\n  intensity: 'border-transparent bg-data-workload-empty-bg text-data-workload-ok-fg',\n  'skill-1': 'border-transparent',\n  'skill-2': 'border-transparent',\n  'skill-3': 'border-transparent',\n  'skill-4': 'border-transparent',\n  'coverage-full': 'border-transparent bg-transparent text-foreground',\n  'coverage-partial': 'border-transparent bg-transparent text-foreground',\n  'coverage-none': 'border-transparent bg-transparent text-muted-foreground',\n} as const;\n\n// Skill level fills per category (colors/data/skill/<category>): levels 1 and 2 use subtle, level 3 the strong fill, level 4 the strong fill plus an inset on-fill ring (TOKEN GAP: no darker fourth fill token yet).\nconst skillClasses: Record<SkillCategory, { subtle: string; strong: string }> = {\n  frontend: { subtle: 'bg-data-skill-frontend-subtle text-data-skill-frontend-on', strong: 'bg-data-skill-frontend-default text-data-skill-frontend-on-fill' },\n  backend: { subtle: 'bg-data-skill-backend-subtle text-data-skill-backend-on', strong: 'bg-data-skill-backend-default text-data-skill-backend-on-fill' },\n  'devops-cloud': { subtle: 'bg-data-skill-devops-cloud-subtle text-data-skill-devops-cloud-on', strong: 'bg-data-skill-devops-cloud-default text-data-skill-devops-cloud-on-fill' },\n  'data-ai': { subtle: 'bg-data-skill-data-ai-subtle text-data-skill-data-ai-on', strong: 'bg-data-skill-data-ai-default text-data-skill-data-ai-on-fill' },\n  'design-ux': { subtle: 'bg-data-skill-design-ux-subtle text-data-skill-design-ux-on', strong: 'bg-data-skill-design-ux-default text-data-skill-design-ux-on-fill' },\n  mobile: { subtle: 'bg-data-skill-mobile-subtle text-data-skill-mobile-on', strong: 'bg-data-skill-mobile-default text-data-skill-mobile-on-fill' },\n  security: { subtle: 'bg-data-skill-security-subtle text-data-skill-security-on', strong: 'bg-data-skill-security-default text-data-skill-security-on-fill' },\n  'testing-qa': { subtle: 'bg-data-skill-testing-qa-subtle text-data-skill-testing-qa-on', strong: 'bg-data-skill-testing-qa-default text-data-skill-testing-qa-on-fill' },\n  infrastructure: { subtle: 'bg-data-skill-infrastructure-subtle text-data-skill-infrastructure-on', strong: 'bg-data-skill-infrastructure-default text-data-skill-infrastructure-on-fill' },\n  'project-management': { subtle: 'bg-data-skill-project-management-subtle text-data-skill-project-management-on', strong: 'bg-data-skill-project-management-default text-data-skill-project-management-on-fill' },\n  'business-strategy': { subtle: 'bg-data-skill-business-strategy-subtle text-data-skill-business-strategy-on', strong: 'bg-data-skill-business-strategy-default text-data-skill-business-strategy-on-fill' },\n  fallback: { subtle: 'bg-data-skill-fallback-subtle text-data-skill-fallback-on', strong: 'bg-data-skill-fallback-default text-data-skill-fallback-on-fill' },\n};\n\n// Inset ring colour per category for level 4 (the on-fill token), so level 4 differs from level 3 without a new fill.\nconst skillRing: Record<SkillCategory, string> = {\n  frontend: 'ring-data-skill-frontend-on-fill',\n  backend: 'ring-data-skill-backend-on-fill',\n  'devops-cloud': 'ring-data-skill-devops-cloud-on-fill',\n  'data-ai': 'ring-data-skill-data-ai-on-fill',\n  'design-ux': 'ring-data-skill-design-ux-on-fill',\n  mobile: 'ring-data-skill-mobile-on-fill',\n  security: 'ring-data-skill-security-on-fill',\n  'testing-qa': 'ring-data-skill-testing-qa-on-fill',\n  infrastructure: 'ring-data-skill-infrastructure-on-fill',\n  'project-management': 'ring-data-skill-project-management-on-fill',\n  'business-strategy': 'ring-data-skill-business-strategy-on-fill',\n  fallback: 'ring-data-skill-fallback-on-fill',\n};\n\nexport type HeatmapTone = keyof typeof heatmapTone;\n\nexport interface HeatmapCellProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'> {\n  tone?: HeatmapTone;\n  // Skill category that colors the skill-1 to skill-4 tones. Defaults to frontend.\n  category?: SkillCategory;\n  selected?: boolean;\n  // Continuous heat from 0 to 1. Setting it switches the cell to the continuous scale (tone `intensity`) and caps the fill so the text keeps 4.5:1 in light and dark.\n  intensity?: number;\n  // Accessible text for tones that show a symbol instead of text, e.g. \"Covered\".\n  label?: string;\n}\n\n// Largest overlay opacity. Computed from the theme tokens: text stays above 4.5:1 up to 0.4 in light and dark.\nconst intensityCap = 0.4;\n\nconst dotClass = {\n  'coverage-full': 'bg-foreground',\n  'coverage-partial': 'border border-solid border-foreground',\n  'coverage-none': 'border border-solid border-hairline',\n} as const;\n\n// Heat cell. A plain element until onClick is set, then a button with the shared focus ring.\nconst HeatmapCell = forwardRef<HTMLButtonElement, HeatmapCellProps>(\n  ({ className, tone: toneProp = 'free', category = 'frontend', selected, intensity, label, children, onClick, type = 'button', ...props }, ref) => {\n    const zero = usePreset() === 'zero';\n    const tone: HeatmapTone = intensity !== undefined ? 'intensity' : toneProp;\n    const dot = tone === 'coverage-full' || tone === 'coverage-partial' || tone === 'coverage-none' ? dotClass[tone] : null;\n    const content = dot ? (\n      <>\n        <span aria-hidden=\"true\" className={cn('block h-2 w-2 rounded-full', dot)} />\n        <span className=\"sr-only\">{label ?? children}</span>\n      </>\n    ) : intensity !== undefined ? (\n      <>\n        <span aria-hidden=\"true\" data-heat-fill=\"\" className=\"absolute inset-0 bg-chart-brand\" style={{ opacity: Math.min(Math.max(intensity, 0), 1) * intensityCap }} />\n        <span className=\"relative\">{children}</span>\n      </>\n    ) : (\n      children\n    );\n    const base = cn(\n      intensity !== undefined && 'relative overflow-hidden',\n      'flex h-heatmap-cell w-full items-center justify-center rounded-item border border-solid tabular-nums',\n      tone === 'skill-1' ? 'typography-text-extra-small font-medium' : tone === 'skill-2' || tone === 'skill-3' || tone === 'skill-4' ? 'typography-ui-medium' : zero ? 'typography-detail' : 'typography-ui',\n      tone === 'skill-1' || tone === 'skill-2' ? skillClasses[category].subtle : tone === 'skill-3' || tone === 'skill-4' ? skillClasses[category].strong : null,\n      tone === 'skill-4' && cn('font-semibold ring-2 ring-inset', skillRing[category]),\n      heatmapTone[tone],\n      selected && 'ring-2 ring-ring',\n      className,\n    );\n    if (!onClick) {\n      return (\n        <span data-tone={tone} data-selected={selected || undefined} className={base}>\n          {content}\n        </span>\n      );\n    }\n    return (\n      <button\n        ref={ref}\n        type={type}\n        onClick={onClick}\n        aria-pressed={selected === undefined ? undefined : selected}\n        data-tone={tone}\n        data-selected={selected || undefined}\n        className={cn(base, 'cursor-pointer hover:brightness-95', transitionBase, focusRing)}\n        {...props}\n      >\n        {content}\n      </button>\n    );\n  },\n);\nHeatmapCell.displayName = 'HeatmapCell';\n\nexport interface HeatmapProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n  'aria-label': string;\n  // Column header labels (weeks, skills, demands).\n  columns: ReactNode[];\n  rowHeaderLabel?: ReactNode;\n  state?: 'default' | 'loading' | 'empty' | 'error';\n  // HeatmapRow elements.\n  children?: ReactNode;\n  emptyTitle?: string;\n  emptyDescription?: string;\n  errorTitle?: string;\n  errorDescription?: string;\n  onRetry?: () => void;\n  loadingLabel?: string;\n  loadingRows?: number;\n}\n\nconst Heatmap = forwardRef<HTMLDivElement, HeatmapProps>(\n  (\n    {\n      className,\n      'aria-label': ariaLabel,\n      columns,\n      rowHeaderLabel: rowHeaderLabelProp,\n      state = 'default',\n      children,\n      emptyTitle: emptyTitleProp,\n      emptyDescription,\n      errorTitle: errorTitleProp,\n      errorDescription: errorDescriptionProp,\n      onRetry,\n      loadingLabel: loadingLabelProp,\n      loadingRows = 3,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels();\n    const rowHeaderLabel = rowHeaderLabelProp ?? l.heatmapRowHeader;\n    const emptyTitle = emptyTitleProp ?? l.noDataYet;\n    const errorTitle = errorTitleProp ?? l.heatmapErrorTitle;\n    const errorDescription = errorDescriptionProp ?? l.dataNotLoaded;\n    const loadingLabel = loadingLabelProp ?? l.heatmapLoading;\n    const span = columns.length + 1;\n    const zero = usePreset() === 'zero';\n    // Zero: the header row is a muted band with 12/500 subtle labels.\n    const headCell = zero ? 'typography-detail bg-muted text-text-subtle-on-muted' : 'typography-text-extra-small font-normal text-muted-foreground';\n    return (\n      <div\n        ref={ref}\n        aria-busy={state === 'loading' || undefined}\n        className={cn('overflow-x-auto rounded-surface border border-solid border-hairline bg-card p-3', className)}\n        {...props}\n      >\n        <table aria-label={ariaLabel} className=\"typography-ui w-full border-separate border-spacing-1 text-foreground\">\n          <thead>\n            <tr>\n              <th scope=\"col\" className={cn('px-2 text-left', headCell)}>\n                {rowHeaderLabel}\n              </th>\n              {columns.map((c, i) => (\n                <th key={i} scope=\"col\" className={cn('px-1 text-center', headCell)}>\n                  {c}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {state === 'default' ? children : null}\n            {state === 'loading'\n              ? Array.from({ length: loadingRows }).map((_, r) => (\n                  <tr key={r}>\n                    {Array.from({ length: span }).map((__, c) => (\n                      <td key={c} className=\"p-0\">\n                        <SkeletonBar className=\"h-heatmap-cell\" />\n                      </td>\n                    ))}\n                  </tr>\n                ))\n              : null}\n            {state === 'empty' || state === 'error' ? (\n              <tr>\n                <td colSpan={span}>\n                  {state === 'empty' ? (\n                    <DataStatePanel kind=\"empty\" icon={<Grid3x3 className=\"h-icon-md w-icon-md\" />} title={emptyTitle} description={emptyDescription} />\n                  ) : (\n                    <DataStatePanel kind=\"error\" title={errorTitle} description={errorDescription} onRetry={onRetry} />\n                  )}\n                </td>\n              </tr>\n            ) : null}\n          </tbody>\n        </table>\n        {state === 'loading' ? (\n          <span role=\"status\" className=\"sr-only\">\n            {loadingLabel}\n          </span>\n        ) : null}\n      </div>\n    );\n  },\n);\nHeatmap.displayName = 'Heatmap';\n\nexport interface HeatmapRowProps extends Omit<HTMLAttributes<HTMLTableRowElement>, 'children'> {\n  header: ReactNode;\n  // One HeatmapCell per column; wrapped in cells automatically.\n  children?: ReactNode;\n}\n\nconst HeatmapRow = forwardRef<HTMLTableRowElement, HeatmapRowProps>(({ className, header, children, ...props }, ref) => (\n  <tr ref={ref} className={className} {...props}>\n    <th scope=\"row\" className=\"typography-ui whitespace-nowrap px-2 text-left font-normal\">\n      {header}\n    </th>\n    {Children.toArray(children).map((child, i) => (\n      <HeatmapTd key={i}>{child}</HeatmapTd>\n    ))}\n  </tr>\n));\nHeatmapRow.displayName = 'HeatmapRow';\n\nconst HeatmapTd = ({ className, ...props }: TdHTMLAttributes<HTMLTableCellElement>) => <td className={cn('p-0 text-center', className)} {...props} />;\n\nexport { Heatmap, HeatmapCell, HeatmapRow };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}