{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "track-list",
  "title": "Track List",
  "description": "Track rows with active, playing and disabled states.",
  "dependencies": [
    "solid-js@^1.9.15",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@audiocn-solid/core"
  ],
  "files": [
    {
      "path": "components/ui/track-list.tsx",
      "content": "import { cva } from \"class-variance-authority\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport { Show, createContext, createMemo, untrack, useContext } from \"solid-js\";\n\nimport { provideContext } from \"@/lib/solid/context\";\nimport type {\n  DivDOMProps,\n  ImageDOMProps,\n  ListDOMProps,\n  ListItemDOMProps,\n  SpanDOMProps,\n  JSXElement,\n} from \"@/lib/solid/jsx-types\";\nimport { forwardProps, omitProps } from \"@/lib/solid/props\";\nimport { cn } from \"@/lib/utils\";\n\nconst ITEM_SELECTOR = \"[data-slot='track-list-item']:not([data-disabled])\";\n\nconst TrackListItemContext = createContext({ active: false, playing: false });\n\nexport const trackListVariants = cva(\"group/track-list flex w-full flex-col\", {\n  defaultVariants: { size: \"default\", variant: \"default\" },\n  variants: {\n    size: {\n      default: \"gap-0.5 [--track-row-height:3rem]\",\n      lg: \"gap-1 [--track-row-height:3.5rem]\",\n      sm: \"gap-0 [--track-row-height:2.25rem]\",\n    },\n    variant: { default: \"\", outline: \"rounded-xl border p-1\" },\n  },\n});\n\nexport interface TrackListProps\n  extends\n    Omit<ListDOMProps, \"onKeyDown\">,\n    VariantProps<typeof trackListVariants> {\n  className?: string;\n  onKeyDown?: (event: KeyboardEvent) => void;\n}\n\nconst moveFocus = (event: KeyboardEvent) => {\n  const root = event.currentTarget;\n  const target = event.target;\n\n  if (\n    !(root instanceof HTMLElement && target instanceof HTMLElement) ||\n    !target.matches(ITEM_SELECTOR)\n  )\n    return;\n  const items = [...root.querySelectorAll<HTMLElement>(ITEM_SELECTOR)];\n  const index = items.indexOf(target);\n\n  const targets = {\n    ArrowDown: index + 1,\n    ArrowUp: index - 1,\n    Home: 0,\n    End: items.length - 1,\n  };\n\n  if (!Object.hasOwn(targets, event.key)) return;\n  // SAFETY: The own-property check proves this is a navigation key.\n  const next = items[targets[event.key as keyof typeof targets]];\n\n  if (next) {\n    event.preventDefault();\n    next.focus();\n  }\n};\n\nexport const TrackList = (props: TrackListProps) => {\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"size\",\n    \"variant\",\n    \"onKeyDown\",\n  ]);\n\n  return (\n    <ul\n      class={cn(\n        trackListVariants({ size: props.size, variant: props.variant }),\n        props.class,\n        props.className\n      )}\n      data-size={props.size ?? \"default\"}\n      data-slot=\"track-list\"\n      onKeyDown={(event: KeyboardEvent) => {\n        props.onKeyDown?.(event);\n\n        if (!event.defaultPrevented) moveFocus(event);\n      }}\n      {...rest}\n    />\n  );\n};\n\nexport type TrackListItemRenderProps = Omit<\n  ListItemDOMProps,\n  \"onClick\" | \"onKeyDown\"\n> & {\n  \"data-slot\": \"track-list-item\";\n  \"data-active\"?: string;\n  \"data-playing\"?: string;\n  \"data-disabled\"?: string;\n  onClick: (event: MouseEvent) => void;\n  onKeyDown: (event: KeyboardEvent) => void;\n};\n\nexport interface TrackListItemProps extends Omit<\n  ListItemDOMProps,\n  \"onClick\" | \"onKeyDown\"\n> {\n  className?: string;\n  active?: boolean;\n  playing?: boolean;\n  disabled?: boolean;\n  onSelect?: () => void;\n  onClick?: (event: MouseEvent) => void;\n  onKeyDown?: (event: KeyboardEvent) => void;\n  render?: (\n    props: TrackListItemRenderProps,\n    state: { readonly active: boolean; readonly playing: boolean }\n  ) => JSXElement;\n}\n\nexport const TrackListItem = (props: TrackListItemProps) => {\n  const rest = omitProps(props, [\n    \"class\",\n    \"className\",\n    \"children\",\n    \"active\",\n    \"playing\",\n    \"disabled\",\n    \"onSelect\",\n    \"onClick\",\n    \"onKeyDown\",\n    \"render\",\n  ]);\n\n  const settings = {\n    get active() {\n      return props.active ?? false;\n    },\n    get playing() {\n      return props.playing ?? false;\n    },\n  };\n\n  const renderProps: TrackListItemRenderProps = {\n    get \"aria-current\"() {\n      return props.active ? \"true\" : undefined;\n    },\n    get \"aria-disabled\"() {\n      return props.disabled ? \"true\" : undefined;\n    },\n    get class() {\n      return cn(\n        \"group/track-list-item hover:bg-muted/60 focus-visible:ring-ring/30 data-active:bg-muted relative flex min-h-(--track-row-height) cursor-default items-center gap-3 rounded-lg px-2 text-sm transition-colors outline-none focus-visible:ring-3 data-disabled:opacity-50\",\n        props.class,\n        props.className\n      );\n    },\n    \"data-slot\": \"track-list-item\",\n    get \"data-active\"() {\n      return props.active ? \"\" : undefined;\n    },\n    get \"data-playing\"() {\n      return props.playing ? \"\" : undefined;\n    },\n    get \"data-disabled\"() {\n      return props.disabled ? \"\" : undefined;\n    },\n    get tabindex() {\n      return props.disabled ? -1 : 0;\n    },\n    onClick: (event: MouseEvent) => {\n      props.onClick?.(event);\n\n      if (event.defaultPrevented || props.disabled) return;\n      const target = event.target;\n\n      const interactive =\n        target instanceof HTMLElement\n          ? target.closest(\n              \"button, a, input, select, textarea, [contenteditable=true]\"\n            )\n          : null;\n\n      if (!interactive || interactive === event.currentTarget)\n        props.onSelect?.();\n    },\n    onKeyDown: (event: KeyboardEvent) => {\n      props.onKeyDown?.(event);\n\n      if (\n        event.defaultPrevented ||\n        props.disabled ||\n        event.target !== event.currentTarget\n      )\n        return;\n\n      if (event.key === \"Enter\" || event.key === \" \") {\n        event.preventDefault();\n        props.onSelect?.();\n      }\n    },\n    get children() {\n      return props.children;\n    },\n  };\n\n  forwardProps(renderProps, rest);\n\n  return provideContext(TrackListItemContext, settings, () => {\n    const customRender = untrack(() => props.render);\n\n    if (customRender) {\n      const rendered = createMemo(() => customRender(renderProps, settings));\n\n      return <>{rendered()}</>;\n    }\n\n    const domProps = omitProps(renderProps, [\"children\"]);\n\n    return <li {...domProps}>{props.children}</li>;\n  });\n};\n\ninterface SpanProps extends SpanDOMProps {\n  className?: string;\n}\n\ninterface DivProps extends DivDOMProps {\n  className?: string;\n}\n\ninterface ImageProps extends ImageDOMProps {\n  className?: string;\n}\n\nexport const TrackListItemIndex = (props: SpanProps) => {\n  const settings = useContext(TrackListItemContext);\n  const rest = omitProps(props, [\"class\", \"className\", \"children\"]);\n\n  return (\n    <span\n      class={cn(\n        \"text-muted-foreground group-data-active/track-list-item:text-primary flex w-5 shrink-0 items-center justify-center font-mono text-xs tabular-nums\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"track-list-item-index\"\n      {...rest}\n    >\n      <Show when={settings.playing} fallback={props.children}>\n        <span aria-label=\"Playing\" role=\"img\" class=\"flex h-3 items-end gap-px\">\n          <span class=\"h-3 w-0.5 animate-pulse rounded-full bg-current motion-reduce:animate-none\" />\n          <span class=\"h-2 w-0.5 animate-pulse rounded-full bg-current [animation-delay:200ms] motion-reduce:animate-none\" />\n          <span class=\"h-2.5 w-0.5 animate-pulse rounded-full bg-current [animation-delay:400ms] motion-reduce:animate-none\" />\n        </span>\n      </Show>\n    </span>\n  );\n};\n\nexport const TrackListItemArtwork = (props: ImageProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <img\n      alt=\"\"\n      class={cn(\n        \"bg-muted size-9 shrink-0 rounded-md object-cover\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"track-list-item-artwork\"\n      {...rest}\n    />\n  );\n};\n\nexport const TrackListItemContent = (props: DivProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <div\n      class={cn(\"flex min-w-0 flex-1 flex-col\", props.class, props.className)}\n      data-slot=\"track-list-item-content\"\n      {...rest}\n    />\n  );\n};\n\nexport const TrackListItemTitle = (props: SpanProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <span\n      class={cn(\n        \"group-data-active/track-list-item:text-primary truncate font-medium\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"track-list-item-title\"\n      {...rest}\n    />\n  );\n};\n\nexport const TrackListItemDescription = (props: SpanProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <span\n      class={cn(\n        \"text-muted-foreground truncate text-xs\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"track-list-item-description\"\n      {...rest}\n    />\n  );\n};\n\nexport const TrackListItemDuration = (props: SpanProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <span\n      class={cn(\n        \"text-muted-foreground shrink-0 font-mono text-xs tabular-nums\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"track-list-item-duration\"\n      {...rest}\n    />\n  );\n};\n\nexport const TrackListItemActions = (props: DivProps) => {\n  const rest = omitProps(props, [\"class\", \"className\"]);\n\n  return (\n    <div\n      class={cn(\n        \"flex shrink-0 items-center gap-1\",\n        props.class,\n        props.className\n      )}\n      data-slot=\"track-list-item-actions\"\n      {...rest}\n    />\n  );\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/audiocn-license.txt",
      "content": "Audio component source adapted from https://github.com/audiocn/ui.\nMIT License — Copyright (c) 2026 OrcDev\n\nRegular SVG icon geometry adapted from @phosphor-icons/react 2.1.10.\nMIT License — Copyright (c) 2020 Phosphor Icons\n\nThe following permission and warranty terms apply to both notices.\nRetain this file with copies or substantial portions of the source.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
      "type": "registry:file",
      "target": "@lib/audiocn-license.txt"
    }
  ],
  "type": "registry:ui"
}
